diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 3d89f6414..000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -name: Bug report -about: Report a bug -labels: bug ---- - - - -## Environment -* OS Version: -* Source or binary build? - - - -## Description -* Expected behavior: -* Actual behavior: - -## Steps to reproduce - - -1. -2. -3. - -## Output - diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index f49727a0c..000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -name: Feature request -about: Request a new feature -labels: enhancement ---- - - - -## Desired behavior - - -## Alternatives considered - - -## Implementation suggestion - - -## Additional context - diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 6c88bd546..000000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,52 +0,0 @@ - - -# Bug Report - -Fixes issue # - -## Summary - - -## Checklist -- [ ] Signed all commits for DCO -- [ ] Added tests -- [ ] Updated documentation (as needed) -- [ ] Updated migration guide (as needed) -- [ ] `codecheck` passed (See - [contributing](https://ignitionrobotics.org/docs/all/contributing#contributing-code)) -- [ ] All tests passed (See - [test coverage](https://ignitionrobotics.org/docs/all/contributing#test-coverage)) -- [ ] While waiting for a review on your PR, please help review -[another open pull request](https://github.com/pulls?q=is%3Aopen+is%3Apr+user%3Aignitionrobotics+archived%3Afalse+) -to support the maintainers - -**Note to maintainers**: Remember to use **Squash-Merge** - ---- - -# New feature - -Closes # - -## Summary - - -## Test it - - -## Checklist -- [ ] Signed all commits for DCO -- [ ] Added tests -- [ ] Added example world and/or tutorial -- [ ] Updated documentation (as needed) -- [ ] Updated migration guide (as needed) -- [ ] `codecheck` passed (See [contributing](https://ignitionrobotics.org/docs/all/contributing#contributing-code)) -- [ ] All tests passed (See [test coverage](https://ignitionrobotics.org/docs/all/contributing#test-coverage)) -- [ ] While waiting for a review on your PR, please help review -[another open pull request](https://github.com/pulls?q=is%3Aopen+is%3Apr+user%3Aignitionrobotics+archived%3Afalse+) -to support the maintainers - -**Note to maintainers**: Remember to use **Squash-Merge** diff --git a/.github/PULL_REQUEST_TEMPLATE/port.md b/.github/PULL_REQUEST_TEMPLATE/port.md deleted file mode 100644 index 5924a4f78..000000000 --- a/.github/PULL_REQUEST_TEMPLATE/port.md +++ /dev/null @@ -1,6 +0,0 @@ -Port to - -Branch comparison: https://github.com/ignitionrobotics/ign-rendering/compare/... - -**Note to maintainers**: Remember to **Merge** with commit (not squash-merge -or rebase) diff --git a/.github/ci/after_make.sh b/.github/ci/after_make.sh index 89971a370..060f55626 100644 --- a/.github/ci/after_make.sh +++ b/.github/ci/after_make.sh @@ -4,7 +4,7 @@ set -x make install -Xvfb :1 -screen 0 1280x1024x24 & +Xvfb :1 -ac -noreset -core -screen 0 1280x1024x24 & export DISPLAY=:1.0 export RENDER_ENGINE_VALUES=ogre2 export MESA_GL_VERSION_OVERRIDE=3.3 diff --git a/Changelog.md b/Changelog.md index 8690b1887..f39430ad9 100644 --- a/Changelog.md +++ b/Changelog.md @@ -12,6 +12,23 @@ ### Ignition Rendering 4.X +### Ignition Rendering 4.5.0 (2021-02-17) + +1. More verbose messages when failing to load render engines + * [Pull Request #236](https://github.com/ignitionrobotics/ign-rendering/pull/236) + +1. Fixed OBJ textures with the same name + * [Pull Request #239](https://github.com/ignitionrobotics/ign-rendering/pull/239) + +1. Fix setting particle size + * [Pull Request #241](https://github.com/ignitionrobotics/ign-rendering/pull/241) + +1. Make particle emitter invisible in thermal camera image + * [Pull Request #240](https://github.com/ignitionrobotics/ign-rendering/pull/240) + +1. Apply particle scattering effect to depth cameras + * [Pull Request #251](https://github.com/ignitionrobotics/ign-rendering/pull/251) + ### Ignition Rendering 4.4.0 (2021-02-11) 1. Add support for 8 bit thermal camera image format diff --git a/examples/particles_demo/Main.cc b/examples/particles_demo/Main.cc index 75efbd1f8..69de0e15d 100644 --- a/examples/particles_demo/Main.cc +++ b/examples/particles_demo/Main.cc @@ -107,6 +107,7 @@ void buildScene(ScenePtr _scene) //! [create particle emitter] ParticleEmitterPtr emitter = _scene->CreateParticleEmitter(); + emitter->SetType(EM_POINT); emitter->SetLocalPose({2, 1.10, 1.25, 1.5708, 0, 2.3}); emitter->SetRate(10); emitter->SetParticleSize({1, 1, 1}); @@ -118,6 +119,21 @@ void buildScene(ScenePtr _scene) emitter->SetEmitting(true); root->AddChild(emitter); //! [create particle emitter] + + // area emitter + ParticleEmitterPtr areaEmitter = _scene->CreateParticleEmitter(); + areaEmitter->SetType(EM_BOX); + areaEmitter->SetEmitterSize({3.0, 3.0, 3.0}); + areaEmitter->SetLocalPose({3, 0, 0, 0, -1.5707, 0}); + areaEmitter->SetRate(10); + areaEmitter->SetParticleSize({0.01, 0.01, 0.01}); + areaEmitter->SetLifetime(1); + areaEmitter->SetVelocityRange(0.5, 1); + areaEmitter->SetMaterial(particleMaterial); + areaEmitter->SetColorRangeImage(RESOURCE_PATH + "/smokecolors.png"); + areaEmitter->SetScaleRate(1); + areaEmitter->SetEmitting(true); + root->AddChild(areaEmitter); } ////////////////////////////////////////////////// diff --git a/ogre2/include/ignition/rendering/ogre2/Ogre2Includes.hh b/ogre2/include/ignition/rendering/ogre2/Ogre2Includes.hh index d25bf7fda..1a0bc41fa 100644 --- a/ogre2/include/ignition/rendering/ogre2/Ogre2Includes.hh +++ b/ogre2/include/ignition/rendering/ogre2/Ogre2Includes.hh @@ -68,6 +68,8 @@ #include #include #include +#include +#include #include #include diff --git a/ogre2/include/ignition/rendering/ogre2/Ogre2ParticleEmitter.hh b/ogre2/include/ignition/rendering/ogre2/Ogre2ParticleEmitter.hh index dc9eb7ba5..2280f5b9f 100644 --- a/ogre2/include/ignition/rendering/ogre2/Ogre2ParticleEmitter.hh +++ b/ogre2/include/ignition/rendering/ogre2/Ogre2ParticleEmitter.hh @@ -86,9 +86,19 @@ namespace ignition public: virtual void SetColorRangeImage(const std::string &_image) override; + /// \brief Particle system visibility flags + public: static const uint32_t kParticleVisibilityFlags; + // Documentation inherited. protected: virtual void Init() override; + /// \brief Internal pre-render function added to avoid breaking ABI + /// compatibility + private: void PreRenderImpl(); + + /// \brief Create the particle system + private: void CreateParticleSystem(); + /// \brief Only the ogre scene can instanstiate this class private: friend class Ogre2Scene; diff --git a/ogre2/src/Ogre2DepthCamera.cc b/ogre2/src/Ogre2DepthCamera.cc index 3b019225f..5f4b12b4a 100644 --- a/ogre2/src/Ogre2DepthCamera.cc +++ b/ogre2/src/Ogre2DepthCamera.cc @@ -31,13 +31,13 @@ #include "ignition/rendering/ogre2/Ogre2DepthCamera.hh" #include "ignition/rendering/ogre2/Ogre2GaussianNoisePass.hh" #include "ignition/rendering/ogre2/Ogre2Includes.hh" +#include "ignition/rendering/ogre2/Ogre2ParticleEmitter.hh" #include "ignition/rendering/ogre2/Ogre2RenderEngine.hh" #include "ignition/rendering/ogre2/Ogre2RenderTarget.hh" #include "ignition/rendering/ogre2/Ogre2RenderTypes.hh" #include "ignition/rendering/ogre2/Ogre2Scene.hh" #include "ignition/rendering/ogre2/Ogre2Sensor.hh" - namespace ignition { namespace rendering @@ -129,6 +129,13 @@ class ignition::rendering::Ogre2DepthCameraPrivate unsigned int, unsigned int, unsigned int, const std::string &)> newDepthFrame; + /// \brief standard deviation of particle noise + public: double particleStddev = 0.01; + + /// \brief Particle scatter ratio. This is used to determine the ratio of + /// particles that will detected by the depth camera + public: double particleScatterRatio = 0.1; + /// \brief Name of sky box material public: const std::string kSkyboxMaterialName = "SkyBox"; }; @@ -424,6 +431,10 @@ void Ogre2DepthCamera::CreateDepthTexture() this->Scene()->BackgroundColor().G(), this->Scene()->BackgroundColor().B()); psParams->setNamedConstant("backgroundColor", bg); + psParams->setNamedConstant("particleStddev", + static_cast(this->dataPtr->particleStddev)); + psParams->setNamedConstant("particleScatterRatio", + static_cast(this->dataPtr->particleScatterRatio)); std::string matDepthFinalName = "DepthCameraFinal"; Ogre::MaterialPtr matDepthFinal = @@ -480,6 +491,22 @@ void Ogre2DepthCamera::CreateDepthTexture() this->dataPtr->ogreCompositorWorkspaceDef = wsDefName; if (!ogreCompMgr->hasWorkspaceDefinition(wsDefName)) { + // The depth camera compositor does a few passes in order to simulate + // particles effects in depth / point cloud image data + // + // render scene (color) with particles, c1 + // render scene (depth) without particles, d1 + // render scene (grayscale) with particles only, g2 + // render scene (depth) with particles only, d2 + // + // if g2 is non-zero // pixel with particle + // if d2 < d1 // particle is in view + // apply noise and scatterbility to d2 + // set depth data to d2 + // else + // set depth data to d1 + // set color data to c1 + // We need to programmatically create the compositor because we need to // configure it to use the cloned depth material created earlier. // The compositor node definition is equivalent to the following: @@ -488,10 +515,12 @@ void Ogre2DepthCamera::CreateDepthTexture() // { // texture rt0 target_width target_height PF_FLOAT32_RGBA // texture rt1 target_width target_height PF_FLOAT32_RGBA - // // colorTexture shares same depth buffer as depthTexture // texture colorTexture target_width target_height PF_R8G8B8 // depth_texture depth_format PF_D32_FLOAT // texture depthTexture target_width target_height PF_D32_FLOAT + // texture particleTexture target_width target_height PF_L8 + // // particleDepthTexture shares same depth buffer as particleTexture + // texture particleDepthTexture target_width target_height PF_D32_FLOAT // target colorTexture // { // pass clear @@ -502,6 +531,37 @@ void Ogre2DepthCamera::CreateDepthTexture() // { // } // } + // target depthTexture + // { + // pass clear + // { + // colour_value 0.0 0.0 0.0 1.0 + // } + // pass render_scene + // { + // visibility_mask 0x11011111 + // } + // } + // target particleTexture + // { + // pass clear + // { + // } + // pass render_scene + // { + // visibility_mask 0x00100000 + // } + // } + // target particleDepthTexture + // { + // pass clear + // { + // } + // pass render_scene + // { + // visibility_mask 0x00100000 + // } + // } // target rt0 // { // pass clear @@ -599,7 +659,45 @@ void Ogre2DepthCamera::CreateDepthTexture() colorTexDef->preferDepthTexture = true; colorTexDef->fsaaExplicitResolve = false; - baseNodeDef->setNumTargetPass(2); + Ogre::TextureDefinitionBase::TextureDefinition *particleTexDef = + baseNodeDef->addTextureDefinition("particleTexture"); + particleTexDef->textureType = Ogre::TEX_TYPE_2D; + particleTexDef->width = 0; + particleTexDef->height = 0; + particleTexDef->depth = 1; + particleTexDef->numMipmaps = 0; + particleTexDef->widthFactor = 0.5; + particleTexDef->heightFactor = 0.5; + particleTexDef->formatList = {Ogre::PF_L8}; + particleTexDef->fsaa = 0; + particleTexDef->uav = false; + particleTexDef->automipmaps = false; + particleTexDef->hwGammaWrite = Ogre::TextureDefinitionBase::BoolFalse; + particleTexDef->depthBufferId = Ogre::DepthBuffer::POOL_DEFAULT; + particleTexDef->depthBufferFormat = Ogre::PF_UNKNOWN; + particleTexDef->preferDepthTexture = false; + particleTexDef->fsaaExplicitResolve = false; + + Ogre::TextureDefinitionBase::TextureDefinition *particleDepthTexDef = + baseNodeDef->addTextureDefinition("particleDepthTexture"); + particleDepthTexDef->textureType = Ogre::TEX_TYPE_2D; + particleDepthTexDef->width = 0; + particleDepthTexDef->height = 0; + particleDepthTexDef->depth = 1; + particleDepthTexDef->numMipmaps = 0; + particleDepthTexDef->widthFactor = 0.5; + particleDepthTexDef->heightFactor = 0.5; + particleDepthTexDef->formatList = {Ogre::PF_D32_FLOAT}; + particleDepthTexDef->fsaa = 0; + particleDepthTexDef->uav = false; + particleDepthTexDef->automipmaps = false; + particleDepthTexDef->hwGammaWrite = Ogre::TextureDefinitionBase::BoolFalse; + particleDepthTexDef->depthBufferId = Ogre::DepthBuffer::POOL_NON_SHAREABLE; + particleDepthTexDef->depthBufferFormat = Ogre::PF_UNKNOWN; + particleDepthTexDef->fsaaExplicitResolve = false; + + baseNodeDef->setNumTargetPass(5); + Ogre::CompositorTargetDef *colorTargetDef = baseNodeDef->addTargetPass("colorTexture"); @@ -631,8 +729,8 @@ void Ogre2DepthCamera::CreateDepthTexture() Ogre::CompositorPassSceneDef *passScene = static_cast( colorTargetDef->addPass(Ogre::PASS_SCENE)); - passScene->mVisibilityMask = IGN_VISIBILITY_ALL - & ~(IGN_VISIBILITY_GUI | IGN_VISIBILITY_SELECTABLE); + passScene->mVisibilityMask = IGN_VISIBILITY_ALL; + // todo(anyone) Fix shadows. The shadow compositor node gets rebuilt // when the number of shadow-casting light changes so we end up with // invalid shadow node here. See Ogre2Scene::PreRender function on how @@ -640,6 +738,64 @@ void Ogre2DepthCamera::CreateDepthTexture() // the number of shadow-casting light changes // passScene->mShadowNode = "PbsMaterialsShadowNode"; } + + Ogre::CompositorTargetDef *depthTargetDef = + baseNodeDef->addTargetPass("depthTexture"); + depthTargetDef->setNumPasses(2); + { + // clear pass + Ogre::CompositorPassClearDef *passClear = + static_cast( + depthTargetDef->addPass(Ogre::PASS_CLEAR)); + passClear->mColourValue = Ogre::ColourValue(this->FarClipPlane(), + this->FarClipPlane(), this->FarClipPlane()); + + // scene pass + Ogre::CompositorPassSceneDef *passScene = + static_cast( + depthTargetDef->addPass(Ogre::PASS_SCENE)); + // depth texute does not contain particles + passScene->mVisibilityMask = IGN_VISIBILITY_ALL + & ~Ogre2ParticleEmitter::kParticleVisibilityFlags; + } + + Ogre::CompositorTargetDef *particleTargetDef = + baseNodeDef->addTargetPass("particleTexture"); + particleTargetDef->setNumPasses(2); + { + // clear pass + Ogre::CompositorPassClearDef *passClear = + static_cast( + particleTargetDef->addPass(Ogre::PASS_CLEAR)); + passClear->mColourValue = Ogre::ColourValue::Black; + + // scene pass + Ogre::CompositorPassSceneDef *passScene = + static_cast( + particleTargetDef->addPass(Ogre::PASS_SCENE)); + passScene->mVisibilityMask = + Ogre2ParticleEmitter::kParticleVisibilityFlags; + } + + Ogre::CompositorTargetDef *particleDepthTargetDef = + baseNodeDef->addTargetPass("particleDepthTexture"); + particleDepthTargetDef->setNumPasses(2); + { + // clear pass + Ogre::CompositorPassClearDef *passClear = + static_cast( + particleDepthTargetDef->addPass(Ogre::PASS_CLEAR)); + passClear->mColourValue = Ogre::ColourValue(this->FarClipPlane(), + this->FarClipPlane(), this->FarClipPlane()); + + // scene pass + Ogre::CompositorPassSceneDef *passScene = + static_cast( + particleDepthTargetDef->addPass(Ogre::PASS_SCENE)); + passScene->mVisibilityMask = + Ogre2ParticleEmitter::kParticleVisibilityFlags; + } + // rt0 target - converts depth to xyz Ogre::CompositorTargetDef *inTargetDef = baseNodeDef->addTargetPass("rt0"); @@ -659,6 +815,8 @@ void Ogre2DepthCamera::CreateDepthTexture() passQuad->mMaterialName = this->dataPtr->depthMaterial->getName(); passQuad->addQuadTextureSource(0, "depthTexture", 0); passQuad->addQuadTextureSource(1, "colorTexture", 0); + passQuad->addQuadTextureSource(2, "particleTexture", 0); + passQuad->addQuadTextureSource(3, "particleDepthTexture", 0); passQuad->mFrustumCorners = Ogre::CompositorPassQuadDef::VIEW_SPACE_CORNERS; } diff --git a/ogre2/src/Ogre2GpuRays.cc b/ogre2/src/Ogre2GpuRays.cc index 2d6e02a98..0d19589fa 100644 --- a/ogre2/src/Ogre2GpuRays.cc +++ b/ogre2/src/Ogre2GpuRays.cc @@ -27,6 +27,7 @@ #include "ignition/rendering/RenderTypes.hh" #include "ignition/rendering/ogre2/Ogre2Conversions.hh" #include "ignition/rendering/ogre2/Ogre2Includes.hh" +#include "ignition/rendering/ogre2/Ogre2ParticleEmitter.hh" #include "ignition/rendering/ogre2/Ogre2RenderTarget.hh" #include "ignition/rendering/ogre2/Ogre2RenderTypes.hh" #include "ignition/rendering/ogre2/Ogre2Scene.hh" @@ -162,6 +163,13 @@ class ignition::rendering::Ogre2GpuRaysPrivate /// \brief Pointer to material switcher public: std::unique_ptr laserRetroMaterialSwitcher[6]; + + /// \brief standard deviation of particle noise + public: double particleStddev = 0.01; + + /// \brief Particle scatter ratio. This is used to determine the ratio of + /// particles that will detected by the depth camera + public: double particleScatterRatio = 0.1; }; using namespace ignition; @@ -607,6 +615,10 @@ void Ogre2GpuRays::Setup1stPass() static_cast(this->dataMaxVal)); psParams->setNamedConstant("min", static_cast(this->dataMinVal)); + psParams->setNamedConstant("particleStddev", + static_cast(this->dataPtr->particleStddev)); + psParams->setNamedConstant("particleScatterRatio", + static_cast(this->dataPtr->particleScatterRatio)); // Create 1st pass compositor auto engine = Ogre2RenderEngine::Instance(); @@ -622,6 +634,8 @@ void Ogre2GpuRays::Setup1stPass() // in 0 rt_input // texture depthTexture target_width target_height PF_D32_FLOAT // texture colorTexture target_width target_height PF_R8G8B8 + // texture particleTexture target_width target_height PF_L8 + // texture particleDepthTexture target_width target_height PF_D32_FLOAT // target colorTexture // { // pass clear @@ -630,6 +644,18 @@ void Ogre2GpuRays::Setup1stPass() // } // pass render_scene // { + // visibility_mask 0x11011111 + // } + // } + // target particleTexture + // { + // pass clear + // { + // colour_value 0.0 0.0 0.0 1.0 + // } + // pass render_scene + // { + // visibility_mask 0.00100000 // } // } // target rt_input @@ -697,7 +723,42 @@ void Ogre2GpuRays::Setup1stPass() colorTexDef->preferDepthTexture = true; colorTexDef->fsaaExplicitResolve = false; - nodeDef->setNumTargetPass(2); + Ogre::TextureDefinitionBase::TextureDefinition *particleDepthTexDef = + nodeDef->addTextureDefinition("particleDepthTexture"); + particleDepthTexDef->textureType = Ogre::TEX_TYPE_2D; + particleDepthTexDef->width = 0; + particleDepthTexDef->height = 0; + particleDepthTexDef->depth = 1; + particleDepthTexDef->numMipmaps = 0; + particleDepthTexDef->widthFactor = 0.5; + particleDepthTexDef->heightFactor = 0.5; + particleDepthTexDef->formatList = {Ogre::PF_D32_FLOAT}; + particleDepthTexDef->fsaa = 0; + particleDepthTexDef->uav = false; + particleDepthTexDef->automipmaps = false; + particleDepthTexDef->hwGammaWrite = Ogre::TextureDefinitionBase::BoolFalse; + particleDepthTexDef->depthBufferId = Ogre::DepthBuffer::POOL_DEFAULT; + + Ogre::TextureDefinitionBase::TextureDefinition *particleTexDef = + nodeDef->addTextureDefinition("particleTexture"); + particleTexDef->textureType = Ogre::TEX_TYPE_2D; + particleTexDef->width = 0; + particleTexDef->height = 0; + particleTexDef->depth = 1; + particleTexDef->numMipmaps = 0; + particleTexDef->widthFactor = 0.5; + particleTexDef->heightFactor = 0.5; + particleTexDef->formatList = {Ogre::PF_R8G8B8}; + particleTexDef->fsaa = 0; + particleTexDef->uav = false; + particleTexDef->automipmaps = false; + particleTexDef->hwGammaWrite = Ogre::TextureDefinitionBase::BoolFalse; + particleTexDef->depthBufferId = Ogre::DepthBuffer::POOL_DEFAULT; + particleTexDef->depthBufferFormat = Ogre::PF_D32_FLOAT; + particleTexDef->preferDepthTexture = true; + particleTexDef->fsaaExplicitResolve = false; + + nodeDef->setNumTargetPass(3); Ogre::CompositorTargetDef *colorTargetDef = nodeDef->addTargetPass("colorTexture"); @@ -713,7 +774,26 @@ void Ogre2GpuRays::Setup1stPass() static_cast( colorTargetDef->addPass(Ogre::PASS_SCENE)); // set camera custom visibility mask when rendering laser retro - passScene->mVisibilityMask = 0x01000000; + passScene->mVisibilityMask = 0x01000000 & + ~Ogre2ParticleEmitter::kParticleVisibilityFlags; + } + + Ogre::CompositorTargetDef *particleTargetDef = + nodeDef->addTargetPass("particleTexture"); + particleTargetDef->setNumPasses(2); + { + // clear pass + Ogre::CompositorPassClearDef *passClear = + static_cast( + particleTargetDef->addPass(Ogre::PASS_CLEAR)); + passClear->mColourValue = Ogre::ColourValue::Black; + // scene pass + Ogre::CompositorPassSceneDef *passScene = + static_cast( + particleTargetDef->addPass(Ogre::PASS_SCENE)); + // set camera custom visibility mask when rendering laser retro + passScene->mVisibilityMask = + Ogre2ParticleEmitter::kParticleVisibilityFlags; } // rt_input target - converts depth to range @@ -733,6 +813,8 @@ void Ogre2GpuRays::Setup1stPass() passQuad->mMaterialName = this->dataPtr->matFirstPass->getName(); passQuad->addQuadTextureSource(0, "depthTexture", 0); passQuad->addQuadTextureSource(1, "colorTexture", 0); + passQuad->addQuadTextureSource(2, "particleDepthTexture", 0); + passQuad->addQuadTextureSource(3, "particleTexture", 0); passQuad->mFrustumCorners = Ogre::CompositorPassQuadDef::VIEW_SPACE_CORNERS; } diff --git a/ogre2/src/Ogre2ParticleEmitter.cc b/ogre2/src/Ogre2ParticleEmitter.cc index 7e0e36f08..39a430eb7 100644 --- a/ogre2/src/Ogre2ParticleEmitter.cc +++ b/ogre2/src/Ogre2ParticleEmitter.cc @@ -36,6 +36,8 @@ using namespace ignition; using namespace rendering; +const uint32_t Ogre2ParticleEmitter::kParticleVisibilityFlags = 0x00100000; + class ignition::rendering::Ogre2ParticleEmitterPrivate { /// \brief Internal material name. @@ -45,8 +47,7 @@ class ignition::rendering::Ogre2ParticleEmitterPrivate public: Ogre::ParticleSystem *ps = nullptr; /// \brief Ogre particle emitter. - public: std::array emitters; + public: Ogre::ParticleEmitter *emitter = nullptr; // \brief Ogre color image affector. public: Ogre::ParticleAffector *colorImageAffector = nullptr; @@ -62,6 +63,10 @@ class ignition::rendering::Ogre2ParticleEmitterPrivate /// \brief Pointer to the unlit material used by particle emitter. public: MaterialPtr materialUnlit; + + /// \brief Flag to indicate that the emitter is dirty and needs to be + /// recreated + public: bool emitterDirty = false; }; // Names used in Ogre for the supported emitters. @@ -91,6 +96,11 @@ void Ogre2ParticleEmitter::Destroy() { if (this->dataPtr->ps) { + this->dataPtr->ps->removeAllAffectors(); + this->dataPtr->colorInterpolatorAffector = nullptr; + this->dataPtr->colorImageAffector = nullptr; + this->dataPtr->scalerAffector = nullptr; + this->scene->OgreSceneManager()->destroyParticleSystem( this->dataPtr->ps); this->dataPtr->ps = nullptr; @@ -104,7 +114,7 @@ void Ogre2ParticleEmitter::Destroy() } ////////////////////////////////////////////////// -void Ogre2ParticleEmitter::Ogre2ParticleEmitter::SetType( +void Ogre2ParticleEmitter::SetType( const EmitterType _type) { // Sanity check: Make sure that the emitter type is valid. @@ -115,11 +125,16 @@ void Ogre2ParticleEmitter::Ogre2ParticleEmitter::SetType( return; } - for (auto i = 0; i < EmitterType::EM_NUM_EMITTERS; ++i) - this->dataPtr->emitters[i]->setEnabled(false); + if (this->type == _type) + return; - this->dataPtr->emitters[_type]->setEnabled(true); this->type = _type; + + this->dataPtr->emitterDirty = true; + // todo(anyone) remove this call. We had to do this since we can't override + // PreRender() as it breaks ABI. We should rename PreRenderImpl to PreRender() + // in next release. + this->PreRenderImpl(); } ////////////////////////////////////////////////// @@ -158,15 +173,13 @@ void Ogre2ParticleEmitter::SetEmitterSize(const ignition::math::Vector3d &_size) for (auto[param, value] : allParamsToSet) { // We skip EM_POINT. - for (auto i = 1; i < EmitterType::EM_NUM_EMITTERS; ++i) + if (!this->dataPtr->emitter->setParameter(param, value)) { - if (!this->dataPtr->emitters[i]->setParameter(param, value)) - { - ignerr << "SetEmitterSize() error for " << kOgreEmitterTypes[i] - << " emitter because SetParameter(" << param << " " << value - << ") failed." << std::endl; - return; - } + ignerr << "SetEmitterSize() error for " + << this->dataPtr->emitter->getType() + << " emitter because SetParameter(" << param << " " << value + << ") failed." << std::endl; + return; } } break; @@ -193,8 +206,7 @@ void Ogre2ParticleEmitter::SetRate(double _rate) return; } - for (auto i = 0; i < EmitterType::EM_NUM_EMITTERS; ++i) - this->dataPtr->emitters[i]->setEmissionRate(_rate); + this->dataPtr->emitter->setEmissionRate(_rate); this->rate = _rate; } @@ -202,8 +214,8 @@ void Ogre2ParticleEmitter::SetRate(double _rate) ////////////////////////////////////////////////// void Ogre2ParticleEmitter::SetDuration(double _duration) { - for (auto i = 0; i < EmitterType::EM_NUM_EMITTERS; ++i) - this->dataPtr->emitters[i]->setDuration(_duration); + this->dataPtr->emitter->setDuration(_duration); + this->duration = _duration; } @@ -211,7 +223,7 @@ void Ogre2ParticleEmitter::SetDuration(double _duration) ////////////////////////////////////////////////// void Ogre2ParticleEmitter::SetEmitting(bool _enable) { - this->dataPtr->emitters[this->type]->setEnabled(_enable); + this->dataPtr->emitter->setEnabled(_enable); this->dataPtr->ps->setEmitting(_enable); this->emitting = _enable; } @@ -227,8 +239,14 @@ void Ogre2ParticleEmitter::SetParticleSize( << "Particle size values should be non-negative." << std::endl; return; } - this->dataPtr->ps->setDefaultDimensions(_size[0], _size[1]); + this->particleSize = _size; + + this->dataPtr->emitterDirty = true; + // todo(anyone) remove this call. We had to do this since we can't override + // PreRender() as it breaks ABI. We should rename PreRenderImpl to PreRender() + // in next release. + this->PreRenderImpl(); } ////////////////////////////////////////////////// @@ -242,8 +260,7 @@ void Ogre2ParticleEmitter::SetLifetime(double _lifetime) return; } - for (auto i = 0; i < EmitterType::EM_NUM_EMITTERS; ++i) - this->dataPtr->emitters[i]->setTimeToLive(_lifetime); + this->dataPtr->emitter->setTimeToLive(_lifetime); this->lifetime = _lifetime; } @@ -267,8 +284,8 @@ void Ogre2ParticleEmitter::SetMaterial(const MaterialPtr &_material) void Ogre2ParticleEmitter::SetVelocityRange(double _minVelocity, double _maxVelocity) { - for (auto i = 0; i < EmitterType::EM_NUM_EMITTERS; ++i) - this->dataPtr->emitters[i]->setParticleVelocity(_minVelocity, _maxVelocity); + this->dataPtr->emitter->setParticleVelocity(_minVelocity, _maxVelocity); + this->minVelocity = _minVelocity; this->maxVelocity = _maxVelocity; @@ -413,27 +430,71 @@ void Ogre2ParticleEmitter::SetColorRangeImage(const std::string &_image) void Ogre2ParticleEmitter::Init() { Ogre2Visual::Init(); + this->CreateParticleSystem(); +} + +////////////////////////////////////////////////// +void Ogre2ParticleEmitter::PreRenderImpl() +{ + // todo(anyone) rename this function to PreRender() so it overrides function + // from base class. Since this rename breaks ABI, we should rename this + // function in the next release + + // recreate the particle system if needed + // currently this is needed when user changes type or particle size + if (this->dataPtr->emitterDirty) + { + this->Destroy(); + this->CreateParticleSystem(); + // make direct ogre calls here so we don't mark emitter as dirty again + this->dataPtr->ps->setDefaultDimensions( + this->particleSize[0], this->particleSize[1]); + + this->SetEmitterSize(this->emitterSize); + + // set other properties + this->SetDuration(this->duration); + this->SetEmitting(this->emitting); + this->SetLifetime(this->lifetime); + this->SetRate(this->rate); + this->SetVelocityRange(this->minVelocity, this->maxVelocity); + + if (this->material) + this->SetMaterial(this->material); + + if (!this->colorRangeImage.empty()) + this->SetColorRangeImage(this->colorRangeImage); + else + this->SetColorRange(this->colorStart, this->colorEnd); + + this->SetScaleRate(this->scaleRate); + + this->dataPtr->emitterDirty = false; + } +} + +////////////////////////////////////////////////// +void Ogre2ParticleEmitter::CreateParticleSystem() +{ // Instantiate the particle system and default parameters. this->dataPtr->ps = this->scene->OgreSceneManager()->createParticleSystem(); this->dataPtr->ps->setCullIndividually(true); this->dataPtr->ps->setParticleQuota(500); this->dataPtr->ps->setSortingEnabled(true); + this->dataPtr->ps->setVisibilityFlags(kParticleVisibilityFlags); + IGN_ASSERT(kOgreEmitterTypes.size() == EmitterType::EM_NUM_EMITTERS, "The nummer of supported emitters does not match the number of " "Ogre emitter types."); - // Instantiate all particle emitters and their default parameters. - // Note that we enable the point emitter by default. - for (auto i = 0; i < EmitterType::EM_NUM_EMITTERS; ++i) - { - this->dataPtr->emitters[i] = - this->dataPtr->ps->addEmitter(kOgreEmitterTypes[i]); - this->dataPtr->emitters[i]->setEnabled(false); - this->dataPtr->emitters[i]->setDirection(Ogre::Vector3::UNIT_X); - } - this->dataPtr->emitters[EmitterType::EM_POINT]->setEnabled(true); + // Instantiate particle emitter and their default parameters. + // Emitter type is point unless otherwise specified. + this->dataPtr->emitter = + this->dataPtr->ps->addEmitter(kOgreEmitterTypes[this->type]); + this->dataPtr->emitter->setDirection(Ogre::Vector3::UNIT_X); + this->dataPtr->emitter->setEnabled(true); // Instantiate the default material. this->dataPtr->materialUnlit = this->scene->CreateMaterial(); @@ -443,8 +504,7 @@ void Ogre2ParticleEmitter::Init() this->dataPtr->ps->setMaterialName( *(this->dataPtr->ogreDatablock->getNameStr())); - // Default emitter parameters. - this->SetParticleSize({1, 1, 1}); + this->dataPtr->ps->setDefaultDimensions(1, 1); this->ogreNode->attachObject(this->dataPtr->ps); igndbg << "Particle emitter initialized" << std::endl; diff --git a/ogre2/src/Ogre2RenderEngine.cc b/ogre2/src/Ogre2RenderEngine.cc index 392770a58..f9df00db6 100644 --- a/ogre2/src/Ogre2RenderEngine.cc +++ b/ogre2/src/Ogre2RenderEngine.cc @@ -676,7 +676,12 @@ void Ogre2RenderEngine::CreateResources() void Ogre2RenderEngine::CreateRenderWindow() { // create dummy window - this->CreateRenderWindow(std::to_string(this->dummyWindowId), 1, 1, 1, 0); + auto res = this->CreateRenderWindow(std::to_string(this->dummyWindowId), + 1, 1, 1, 0); + if (res.empty()) + { + ignerr << "Failed to create dummy render window." << std::endl; + } } ////////////////////////////////////////////////// @@ -736,16 +741,18 @@ std::string Ogre2RenderEngine::CreateRenderWindow(const std::string &_handle, window = this->ogreRoot->createRenderWindow( stream.str(), _width, _height, false, ¶ms); } - catch(...) + catch(const std::exception &_e) { - ignerr << " Unable to create the rendering window\n"; + ignerr << " Unable to create the rendering window: " << _e.what() + << std::endl; window = nullptr; } } if (attempts >= 10) { - ignerr << "Unable to create the rendering window\n" << std::endl; + ignerr << "Unable to create the rendering window after [" << attempts + << "] attempts." << std::endl; return std::string(); } diff --git a/ogre2/src/Ogre2Scene.cc b/ogre2/src/Ogre2Scene.cc index 43816ec02..5c4619bdf 100644 --- a/ogre2/src/Ogre2Scene.cc +++ b/ogre2/src/Ogre2Scene.cc @@ -478,6 +478,7 @@ ParticleEmitterPtr Ogre2Scene::CreateParticleEmitterImpl(unsigned int _id, { Ogre2ParticleEmitterPtr visual(new Ogre2ParticleEmitter); bool result = this->InitObject(visual, _id, _name); + return (result) ? visual : nullptr; } diff --git a/ogre2/src/Ogre2ThermalCamera.cc b/ogre2/src/Ogre2ThermalCamera.cc index 46ffa5339..eaac4647e 100644 --- a/ogre2/src/Ogre2ThermalCamera.cc +++ b/ogre2/src/Ogre2ThermalCamera.cc @@ -48,6 +48,7 @@ #include "ignition/rendering/ogre2/Ogre2Conversions.hh" #include "ignition/rendering/ogre2/Ogre2Includes.hh" #include "ignition/rendering/ogre2/Ogre2Material.hh" +#include "ignition/rendering/ogre2/Ogre2ParticleEmitter.hh" #include "ignition/rendering/ogre2/Ogre2RenderEngine.hh" #include "ignition/rendering/ogre2/Ogre2RenderTarget.hh" #include "ignition/rendering/ogre2/Ogre2RenderTypes.hh" @@ -737,7 +738,12 @@ void Ogre2ThermalCamera::CreateThermalTexture() colorTargetDef->addPass(Ogre::PASS_CLEAR)); passClear->mColourValue = Ogre::ColourValue(0, 0, 0); // scene pass - colorTargetDef->addPass(Ogre::PASS_SCENE); + Ogre::CompositorPassSceneDef *passScene = + static_cast( + colorTargetDef->addPass(Ogre::PASS_SCENE)); + // thermal camera should not see particles + passScene->mVisibilityMask = IGN_VISIBILITY_ALL & + ~Ogre2ParticleEmitter::kParticleVisibilityFlags; } // rt_input target - converts to thermal diff --git a/ogre2/src/Ogre2Visual.cc b/ogre2/src/Ogre2Visual.cc index 262985de0..f6eefeac2 100644 --- a/ogre2/src/Ogre2Visual.cc +++ b/ogre2/src/Ogre2Visual.cc @@ -19,6 +19,7 @@ #include "ignition/rendering/ogre2/Ogre2Conversions.hh" #include "ignition/rendering/ogre2/Ogre2Geometry.hh" +#include "ignition/rendering/ogre2/Ogre2ParticleEmitter.hh" #include "ignition/rendering/ogre2/Ogre2RenderTypes.hh" #include "ignition/rendering/ogre2/Ogre2Storage.hh" #include "ignition/rendering/ogre2/Ogre2Visual.hh" @@ -59,7 +60,10 @@ void Ogre2Visual::SetVisibilityFlags(uint32_t _flags) return; for (unsigned int i = 0; i < this->ogreNode->numAttachedObjects(); ++i) - this->ogreNode->getAttachedObject(i)->setVisibilityFlags(_flags); + { + this->ogreNode->getAttachedObject(i)->setVisibilityFlags(_flags + & ~Ogre2ParticleEmitter::kParticleVisibilityFlags); + } } ////////////////////////////////////////////////// @@ -100,7 +104,8 @@ bool Ogre2Visual::AttachGeometry(GeometryPtr _geometry) ogreObj->getUserObjectBindings().setUserAny( Ogre::Any(this->Id())); ogreObj->setName(this->Name() + "_" + _geometry->Name()); - ogreObj->setVisibilityFlags(this->visibilityFlags); + ogreObj->setVisibilityFlags(this->visibilityFlags + & ~Ogre2ParticleEmitter::kParticleVisibilityFlags); derived->SetParent(this->SharedThis()); this->ogreNode->attachObject(ogreObj); diff --git a/ogre2/src/media/materials/programs/depth_camera_fs.glsl b/ogre2/src/media/materials/programs/depth_camera_fs.glsl index bb195a7b4..856f7aa1f 100644 --- a/ogre2/src/media/materials/programs/depth_camera_fs.glsl +++ b/ogre2/src/media/materials/programs/depth_camera_fs.glsl @@ -25,6 +25,8 @@ in block uniform sampler2D depthTexture; uniform sampler2D colorTexture; +uniform sampler2D particleTexture; +uniform sampler2D particleDepthTexture; out vec4 fragColor; @@ -35,12 +37,9 @@ uniform float min; uniform float max; uniform vec3 backgroundColor; -float getDepth(vec2 uv) -{ - float fDepth = texture(depthTexture, uv).x; - float linearDepth = projectionParams.y / (fDepth - projectionParams.x); - return linearDepth; -} +uniform float particleStddev; +uniform float particleScatterRatio; +uniform float time; float packFloat(vec4 color) { @@ -52,12 +51,43 @@ float packFloat(vec4 color) } +// see gaussian_noise_fs.glsl for documentation on the rand and gaussrand +// functions + +#define PI 3.14159265358979323846264 + +float rand(vec2 co) +{ + float r = fract(sin(dot(co.xy, vec2(12.9898,78.233))) * 43758.5453); + // Make sure that we don't return 0.0 + if(r == 0.0) + return 0.000000000001; + else + return r; +} + +vec4 gaussrand(vec2 co, vec3 offsets, float stddev, float mean) +{ + float U, V, R, Z; + U = rand(co + vec2(offsets.x, offsets.x)); + V = rand(co + vec2(offsets.y, offsets.y)); + R = rand(co + vec2(offsets.z, offsets.z)); + if(R < 0.5) + Z = sqrt(-2.0 * log(U)) * sin(2.0 * PI * V); + else + Z = sqrt(-2.0 * log(U)) * cos(2.0 * PI * V); + Z = Z * stddev + mean; + return vec4(Z, Z, Z, 0.0); +} + void main() { float tolerance = 1e-6; // get linear depth - float d = getDepth(inPs.uv0); + float fDepth = texture(depthTexture, inPs.uv0).x; + float d = projectionParams.y / (fDepth - projectionParams.x); + // reconstruct 3d viewspace pos from depth vec3 viewSpacePos = inPs.cameraDir * d; @@ -68,6 +98,30 @@ void main() // color vec4 color = texture(colorTexture, inPs.uv0); + // particle mask - color and depth + vec4 particle = texture(particleTexture, inPs.uv0); + float particleDepth = texture(particleDepthTexture, inPs.uv0).x; + + // return particle depth if it can be seen by the camera and not obstructed + // by other objects in the camera view + if (particle.x > 0 && particleDepth > 0.0 && particleDepth < fDepth) + { + // apply scatter effect so that only some of the smoke pixels are visible + float r = rand(inPs.uv0 + vec2(time, time)); + if (r < particleScatterRatio) + { + // set point to 3d pos of particle pixel + float pd = projectionParams.y / (particleDepth - projectionParams.x); + vec3 particleViewSpacePos = inPs.cameraDir * pd; + point = vec3(-particleViewSpacePos.z, -particleViewSpacePos.x, + particleViewSpacePos.y); + + // apply gaussian noise to particle depth data + point = point + gaussrand(inPs.uv0, vec3(time, time, time), + particleStddev, 0.0).xyz; + } + } + // clamp xyz and set rgb to background color if (point.x > far - tolerance) { @@ -79,7 +133,14 @@ void main() { point.x = max; } - color = vec4(backgroundColor, 1.0); + // clamp to background color only if it is not a particle pixel + // this is because point.x may have been set to background depth value + // due to the scatter effect. We should still render particles in the color + // image + if (particle.x < 1e-6) + { + color = vec4(backgroundColor, 1.0); + } } else if (point.x < near + tolerance) { @@ -91,6 +152,7 @@ void main() { point.x = min; } + color = vec4(backgroundColor, 1.0); } diff --git a/ogre2/src/media/materials/programs/gpu_rays_1st_pass_fs.glsl b/ogre2/src/media/materials/programs/gpu_rays_1st_pass_fs.glsl index d01d153ac..410e8511f 100644 --- a/ogre2/src/media/materials/programs/gpu_rays_1st_pass_fs.glsl +++ b/ogre2/src/media/materials/programs/gpu_rays_1st_pass_fs.glsl @@ -25,6 +25,8 @@ in block uniform sampler2D depthTexture; uniform sampler2D colorTexture; +uniform sampler2D particleDepthTexture; +uniform sampler2D particleTexture; out vec4 fragColor; @@ -34,19 +36,45 @@ uniform float far; uniform float min; uniform float max; -float getDepth(vec2 uv) +uniform float particleStddev; +uniform float particleScatterRatio; +uniform float time; + +// see gaussian_noise_fs.glsl for documentation on the rand and gaussrand +// functions + +#define PI 3.14159265358979323846264 + +float rand(vec2 co) { - float fDepth = texture(depthTexture, uv).x; - float linearDepth = projectionParams.y / (fDepth - projectionParams.x); - return linearDepth; + float r = fract(sin(dot(co.xy, vec2(12.9898,78.233))) * 43758.5453); + // Make sure that we don't return 0.0 + if(r == 0.0) + return 0.000000000001; + else + return r; } +vec4 gaussrand(vec2 co, vec3 offsets, float stddev, float mean) +{ + float U, V, R, Z; + U = rand(co + vec2(offsets.x, offsets.x)); + V = rand(co + vec2(offsets.y, offsets.y)); + R = rand(co + vec2(offsets.z, offsets.z)); + if(R < 0.5) + Z = sqrt(-2.0 * log(U)) * sin(2.0 * PI * V); + else + Z = sqrt(-2.0 * log(U)) * cos(2.0 * PI * V); + Z = Z * stddev + mean; + return vec4(Z, Z, Z, 0.0); +} void main() { // get linear depth - float d = getDepth(inPs.uv0); - + float fDepth = texture(depthTexture, inPs.uv0).x; + float d = projectionParams.y / (fDepth - projectionParams.x); + // get retro float retro = texture(colorTexture, inPs.uv0).x * 2000.0; @@ -56,6 +84,27 @@ void main() // get length of 3d point, i.e.range float l = length(viewSpacePos); + // particle mask - color and depth + vec4 particle = texture(particleTexture, inPs.uv0); + float particleDepth = texture(particleDepthTexture, inPs.uv0).x; + + if (particle.x > 0.0 && particleDepth > 0.0 && particleDepth < fDepth) + { + // apply scatter effect so that only some of the smoke pixels are visible + float r = rand(inPs.uv0 + vec2(time, time)); + if (r < particleScatterRatio) + { + float pd = projectionParams.y / (particleDepth - projectionParams.x); + vec3 point = inPs.cameraDir * pd; + + // apply gaussian noise to particle depth data + point = point + gaussrand(inPs.uv0, vec3(time, time, time), + particleStddev, 0.0).xyz; + + l = length(point); + } + } + if (l > far) l = max; else if (l < near) diff --git a/ogre2/src/media/materials/programs/gpu_rays_2nd_pass_fs.glsl b/ogre2/src/media/materials/programs/gpu_rays_2nd_pass_fs.glsl index dab691092..ec4bb0b07 100644 --- a/ogre2/src/media/materials/programs/gpu_rays_2nd_pass_fs.glsl +++ b/ogre2/src/media/materials/programs/gpu_rays_2nd_pass_fs.glsl @@ -87,9 +87,9 @@ void main() else if (faceIdx == 5) d = getRange(uv, tex5); - // todo(anyone) set retro values - float retro = 0.0; + float range = d.x; + float retro = d.y; - fragColor = vec4(d.x, d.y, 0, 1.0); + fragColor = vec4(range, retro, 0, 1.0); return; } diff --git a/ogre2/src/media/materials/scripts/depth_camera.material b/ogre2/src/media/materials/scripts/depth_camera.material index cda34c985..e0b87e166 100644 --- a/ogre2/src/media/materials/scripts/depth_camera.material +++ b/ogre2/src/media/materials/scripts/depth_camera.material @@ -30,8 +30,12 @@ fragment_program DepthCameraFS glsl default_params { + param_named_auto time time + param_named depthTexture int 0 param_named colorTexture int 1 + param_named particleTexture int 2 + param_named particleDepthTexture int 3 } } @@ -53,6 +57,16 @@ material DepthCamera filtering none tex_address_mode clamp } + texture_unit particleTexture + { + filtering none + tex_address_mode clamp + } + texture_unit particleDepthTexture + { + filtering none + tex_address_mode clamp + } } } } diff --git a/ogre2/src/media/materials/scripts/gpu_rays.material b/ogre2/src/media/materials/scripts/gpu_rays.material index 5730b8ddd..fb027f569 100644 --- a/ogre2/src/media/materials/scripts/gpu_rays.material +++ b/ogre2/src/media/materials/scripts/gpu_rays.material @@ -30,8 +30,11 @@ fragment_program GpuRaysScan1stFS glsl default_params { + param_named_auto time time param_named depthTexture int 0 param_named colorTexture int 1 + param_named particleDepthTexture int 2 + param_named particleTexture int 3 } } @@ -53,6 +56,16 @@ material GpuRaysScan1st filtering none tex_address_mode clamp } + texture_unit particleDepthTexture + { + filtering none + tex_address_mode clamp + } + texture_unit particleTexture + { + filtering none + tex_address_mode clamp + } } } } diff --git a/test/integration/camera.cc b/test/integration/camera.cc index 7ab3dd0cb..26e840ce2 100644 --- a/test/integration/camera.cc +++ b/test/integration/camera.cc @@ -245,12 +245,13 @@ void CameraTest::VisualAt(const std::string &_renderEngine) if (x <= 100) { - EXPECT_EQ(nullptr, vis); + EXPECT_EQ(nullptr, vis) + << "Found [" << vis->Name() << "] at X [" << x << "]"; } else if (x > 100 && x <= 300) { // Don't end test here on failure, this condition is flaky - EXPECT_NE(nullptr, vis) << x; + EXPECT_NE(nullptr, vis) << "X: " << x; if (vis) { EXPECT_EQ("sphere", vis->Name()); @@ -258,12 +259,13 @@ void CameraTest::VisualAt(const std::string &_renderEngine) } else if (x > 300 && x <= 400) { - EXPECT_EQ(nullptr, vis); + EXPECT_EQ(nullptr, vis) + << "Found [" << vis->Name() << "] at X [" << x << "]"; } else if (x > 400 && x <= 700) { // Don't end test here on failure, this condition is flaky - EXPECT_NE(nullptr, vis) << x; + EXPECT_NE(nullptr, vis) << "X: " << x; if (vis) { EXPECT_EQ("box", vis->Name()); diff --git a/test/integration/depth_camera.cc b/test/integration/depth_camera.cc index b67c7d9af..aa79648c5 100644 --- a/test/integration/depth_camera.cc +++ b/test/integration/depth_camera.cc @@ -24,6 +24,7 @@ #include "test_config.h" // NOLINT(build/include) #include "ignition/rendering/DepthCamera.hh" +#include "ignition/rendering/ParticleEmitter.hh" #include "ignition/rendering/RenderEngine.hh" #include "ignition/rendering/RenderingIface.hh" #include "ignition/rendering/Scene.hh" @@ -61,6 +62,10 @@ class DepthCameraTest: public testing::Test, { // Create a Camera sensor from a SDF and gets a image message public: void DepthCameraBoxes(const std::string &_renderEngine); + + // Compare depth camera image before and after adding particles + // in the scene + public: void DepthCameraParticles(const std::string &_renderEngine); }; void DepthCameraTest::DepthCameraBoxes( @@ -458,6 +463,233 @@ void DepthCameraTest::DepthCameraBoxes( ignition::rendering::unloadEngine(engine->Name()); } + +void DepthCameraTest::DepthCameraParticles( + const std::string &_renderEngine) +{ + int imgWidth_ = 256; + int imgHeight_ = 256; + double aspectRatio_ = imgWidth_ / imgHeight_; + + // box should fill camera view + // we will add particle emitter in between box and depth camera later + ignition::math::Vector3d boxSize(1.0, 10.0, 10.0); + ignition::math::Vector3d boxPosition(1.8, 0.0, 0.0); + + // particle emitter is only supported in ogre2 + if (_renderEngine.compare("ogre2") != 0) + { + igndbg << "Engine '" << _renderEngine + << "' doesn't support depth cameras" << std::endl; + return; + } + + // Setup ign-rendering with an empty scene + auto *engine = ignition::rendering::engine(_renderEngine); + if (!engine) + { + igndbg << "Engine '" << _renderEngine + << "' is not supported" << std::endl; + return; + } + + ignition::rendering::ScenePtr scene = engine->CreateScene("scene"); + + // red background + scene->SetBackgroundColor(1.0, 0.0, 0.0); + + // Create an scene with a box in it + scene->SetAmbientLight(1.0, 1.0, 1.0); + ignition::rendering::VisualPtr root = scene->RootVisual(); + + // create blue material + ignition::rendering::MaterialPtr blue = scene->CreateMaterial(); + blue->SetAmbient(0.0, 0.0, 1.0); + blue->SetDiffuse(0.0, 0.0, 1.0); + blue->SetSpecular(0.0, 0.0, 1.0); + + // create box visual + ignition::rendering::VisualPtr box = scene->CreateVisual(); + box->AddGeometry(scene->CreateBox()); + box->SetOrigin(0.0, 0.0, 0.0); + box->SetLocalPosition(boxPosition); + box->SetLocalRotation(0, 0, 0); + box->SetLocalScale(boxSize); + box->SetMaterial(blue); + root->AddChild(box); + { + double farDist = 10.0; + double nearDist = 0.01; + double hfov_ = 1.05; + // Create depth camera + auto depthCamera = scene->CreateDepthCamera("DepthCamera"); + ASSERT_NE(depthCamera, nullptr); + + ignition::math::Pose3d testPose(ignition::math::Vector3d(0, 0, 0), + ignition::math::Quaterniond::Identity); + depthCamera->SetLocalPose(testPose); + + // Configure depth camera + depthCamera->SetImageWidth(imgWidth_); + EXPECT_EQ(depthCamera->ImageWidth(), + static_cast(imgWidth_)); + depthCamera->SetImageHeight(imgHeight_); + EXPECT_EQ(depthCamera->ImageHeight(), + static_cast(imgHeight_)); + depthCamera->SetFarClipPlane(farDist); + EXPECT_DOUBLE_EQ(depthCamera->FarClipPlane(), farDist); + depthCamera->SetNearClipPlane(nearDist); + EXPECT_DOUBLE_EQ(depthCamera->NearClipPlane(), nearDist); + depthCamera->SetAspectRatio(aspectRatio_); + EXPECT_DOUBLE_EQ(depthCamera->AspectRatio(), aspectRatio_); + depthCamera->SetHFOV(hfov_); + EXPECT_DOUBLE_EQ(depthCamera->HFOV().Radian(), hfov_); + + depthCamera->CreateDepthTexture(); + scene->RootVisual()->AddChild(depthCamera); + + // Set a callback on the camera sensor to get a depth camera frame + float *scan = new float[imgHeight_ * imgWidth_]; + ignition::common::ConnectionPtr connection = + depthCamera->ConnectNewDepthFrame( + std::bind(&::OnNewDepthFrame, scan, + std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, + std::placeholders::_4, std::placeholders::_5)); + + // rgb point cloud data callback + unsigned int pointCloudChannelCount = 4u; + float *pointCloudData = new float[ + imgHeight_ * imgWidth_ * pointCloudChannelCount]; + ignition::common::ConnectionPtr connection2 = + depthCamera->ConnectNewRgbPointCloud( + std::bind(&::OnNewRgbPointCloud, pointCloudData, + std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, + std::placeholders::_4, std::placeholders::_5)); + + // update and verify we get new data + g_depthCounter = 0u; + g_pointCloudCounter = 0u; + depthCamera->Update(); + EXPECT_EQ(1u, g_depthCounter); + EXPECT_EQ(1u, g_pointCloudCounter); + + double expectedDepth = boxPosition.X() - boxSize.X() * 0.5; + + double pointAvg = 0.0; + double depthAvg = 0.0; + // Verify depth and point cloud data before particle effects + for (unsigned int i = 0u; i < depthCamera->ImageHeight(); ++i) + { + unsigned int step = + i * depthCamera->ImageWidth() * pointCloudChannelCount; + for (unsigned int j = 0u; j < depthCamera->ImageWidth(); ++j) + { + float x = pointCloudData[step + j * pointCloudChannelCount]; + float y = pointCloudData[step + j * pointCloudChannelCount + 1]; + float z = pointCloudData[step + j * pointCloudChannelCount + 2]; + EXPECT_NEAR(expectedDepth, x, DEPTH_TOL); + float d = scan[i * depthCamera->ImageWidth() + j]; + EXPECT_NEAR(expectedDepth, d, DEPTH_TOL); + + pointAvg += ignition::math::Vector3d(x, y, z).Length(); + depthAvg += d; + } + } + + // create particle emitter between depth camera and box + ignition::math::Vector3d particlePosition(1.0, 0, 0); + ignition::math::Vector3d particleSize(0.2, 0.2, 0.2); + ignition::rendering::ParticleEmitterPtr emitter = + scene->CreateParticleEmitter(); + emitter->SetLocalPosition(particlePosition); + emitter->SetParticleSize(particleSize); + emitter->SetRate(100); + emitter->SetLifetime(2); + emitter->SetVelocityRange(0.1, 0.1); + emitter->SetScaleRate(0.2); + emitter->SetColorRange(ignition::math::Color::Red, + ignition::math::Color::Black); + emitter->SetEmitting(true); + root->AddChild(emitter); + + // update and verify we get new data + // make sure to update for a few frames for particles for flow into + // camera view. + g_depthCounter = 0u; + g_pointCloudCounter = 0u; + for (unsigned int i = 0; i < 100; ++i) + { + depthCamera->Update(); + } + EXPECT_EQ(100u, g_depthCounter); + EXPECT_EQ(100u, g_pointCloudCounter); + + double pointParticleAvg = 0.0; + double depthParticleAvg = 0.0; + + // set a larger tol for particle depth + // tol is particle size + 4 sigma of noise stddev + double noiseStddev = 0.01; + double depthNoiseTol = particleSize.X() + 4 * noiseStddev;; + double expectedParticleDepth = particlePosition.X(); + + // Verify depth and point cloud data after particle effects + for (unsigned int i = 0u; i < depthCamera->ImageHeight(); ++i) + { + unsigned int step = + i * depthCamera->ImageWidth() * pointCloudChannelCount; + for (unsigned int j = 0u; j < depthCamera->ImageWidth(); ++j) + { + float x = pointCloudData[step + j * pointCloudChannelCount]; + float y = pointCloudData[step + j * pointCloudChannelCount + 1]; + float z = pointCloudData[step + j * pointCloudChannelCount + 2]; + + double xd = static_cast(x); + // depth camera sees only certain percentage of particles + // so the values should be either + // * box depth (depth camera does not see particles), or + // * noisy particle depth (depth camera see particles but values + // are affected by noise) + EXPECT_TRUE( + ignition::math::equal(expectedParticleDepth, xd, depthNoiseTol) || + ignition::math::equal(expectedDepth, xd, DEPTH_TOL)) + << "actual vs expected particle depth: " + << xd << " vs " << expectedParticleDepth; + float depth = scan[i * depthCamera->ImageWidth() + j]; + double depthd = static_cast(depth); + EXPECT_TRUE( + ignition::math::equal(expectedParticleDepth, depthd, depthNoiseTol) + || ignition::math::equal(expectedDepth, depthd, DEPTH_TOL)) + << "actual vs expected particle depth: " + << depthd << " vs " << expectedParticleDepth; + + pointParticleAvg += ignition::math::Vector3d(x, y, z).Length(); + depthParticleAvg += depthd; + } + } + + // compare point and depth data before and after particle effects + // the avg point length and depth values in the image with particle effects + // should be lower than the image without particle effects + double pixelCount = depthCamera->ImageWidth() * depthCamera->ImageHeight(); + pointAvg /= pixelCount; + depthAvg /= pixelCount; + pointParticleAvg /= pixelCount; + depthParticleAvg /= pixelCount; + EXPECT_LT(pointParticleAvg, pointAvg); + EXPECT_LT(depthParticleAvg, depthAvg); + + // Clean up + connection.reset(); + delete [] scan; + if (pointCloudData) + delete [] pointCloudData; + } + + engine->DestroyScene(scene); + ignition::rendering::unloadEngine(engine->Name()); +} + #ifdef __APPLE__ TEST_P(DepthCameraTest, DISABLED_DepthCameraBoxes) #else @@ -467,6 +699,15 @@ TEST_P(DepthCameraTest, DepthCameraBoxes) DepthCameraBoxes(GetParam()); } +#ifdef __APPLE__ +TEST_P(DepthCameraTest, DISABLED_DepthCameraParticles) +#else +TEST_P(DepthCameraTest, DepthCameraParticles) +#endif +{ + DepthCameraParticles(GetParam()); +} + INSTANTIATE_TEST_CASE_P(DepthCamera, DepthCameraTest, RENDER_ENGINE_VALUES, ignition::rendering::PrintToStringParam()); diff --git a/test/integration/gpu_rays.cc b/test/integration/gpu_rays.cc index 6a3c58d93..835bba26d 100644 --- a/test/integration/gpu_rays.cc +++ b/test/integration/gpu_rays.cc @@ -24,6 +24,7 @@ #include "test_config.h" // NOLINT(build/include) #include "ignition/rendering/GpuRays.hh" +#include "ignition/rendering/ParticleEmitter.hh" #include "ignition/rendering/RenderEngine.hh" #include "ignition/rendering/RenderingIface.hh" #include "ignition/rendering/Scene.hh" @@ -60,6 +61,9 @@ class GpuRaysTest: public testing::Test, // Test vertical measurements public: void LaserVertical(const std::string &_renderEngine); + + // Test detection of particles + public: void RaysParticles(const std::string &_renderEngine); }; ///////////////////////////////////////////////// @@ -472,6 +476,173 @@ void GpuRaysTest::LaserVertical(const std::string &_renderEngine) rendering::unloadEngine(engine->Name()); } +///////////////////////////////////////////////// +/// \brief Test detection of particles +void GpuRaysTest::RaysParticles(const std::string &_renderEngine) +{ +#ifdef __APPLE__ + std::cerr << "Skipping test for apple, see issue #35." << std::endl; + return; +#endif + + if (_renderEngine != "ogre2") + { + igndbg << "GpuRays with particle effect is not supported yet in rendering " + << "engine: " << _renderEngine << std::endl; + return; + } + + // Test GPU ray with 3 boxes in the world. + // Add noise in btewen GPU ray and box in the center + + const double hMinAngle = -IGN_PI / 2.0; + const double hMaxAngle = IGN_PI / 2.0; + const double minRange = 0.1; + const double maxRange = 10.0; + const int hRayCount = 320; + const int vRayCount = 1; + + // create and populate scene + RenderEngine *engine = rendering::engine(_renderEngine); + if (!engine) + { + igndbg << "Engine '" << _renderEngine + << "' is not supported" << std::endl; + return; + } + + ScenePtr scene = engine->CreateScene("scene"); + ASSERT_TRUE(scene != nullptr); + + VisualPtr root = scene->RootVisual(); + + // Create ray caster + ignition::math::Pose3d testPose(ignition::math::Vector3d(0, 0, 0.1), + ignition::math::Quaterniond::Identity); + + GpuRaysPtr gpuRays = scene->CreateGpuRays("gpu_rays_1"); + gpuRays->SetWorldPosition(testPose.Pos()); + gpuRays->SetWorldRotation(testPose.Rot()); + gpuRays->SetNearClipPlane(minRange); + gpuRays->SetFarClipPlane(maxRange); + gpuRays->SetAngleMin(hMinAngle); + gpuRays->SetAngleMax(hMaxAngle); + gpuRays->SetRayCount(hRayCount); + + gpuRays->SetVerticalRayCount(vRayCount); + root->AddChild(gpuRays); + + // Create testing boxes + // box in the center + ignition::math::Pose3d box01Pose(ignition::math::Vector3d(3, 0, 0.5), + ignition::math::Quaterniond::Identity); + VisualPtr visualBox1 = scene->CreateVisual("UnitBox1"); + visualBox1->AddGeometry(scene->CreateBox()); + visualBox1->SetWorldPosition(box01Pose.Pos()); + visualBox1->SetWorldRotation(box01Pose.Rot()); + root->AddChild(visualBox1); + + // box on the right of the first gpu rays caster + ignition::math::Pose3d box02Pose(ignition::math::Vector3d(0, -5, 0.5), + ignition::math::Quaterniond::Identity); + VisualPtr visualBox2 = scene->CreateVisual("UnitBox2"); + visualBox2->AddGeometry(scene->CreateBox()); + visualBox2->SetWorldPosition(box02Pose.Pos()); + visualBox2->SetWorldRotation(box02Pose.Rot()); + root->AddChild(visualBox2); + + // box on the left of the rays caster 1 but out of range + ignition::math::Pose3d box03Pose( + ignition::math::Vector3d(0, maxRange + 1, 0.5), + ignition::math::Quaterniond::Identity); + VisualPtr visualBox3 = scene->CreateVisual("UnitBox3"); + visualBox3->AddGeometry(scene->CreateBox()); + visualBox3->SetWorldPosition(box03Pose.Pos()); + visualBox3->SetWorldRotation(box03Pose.Rot()); + root->AddChild(visualBox3); + + // create particle emitter between sensor and box in the center + ignition::math::Vector3d particlePosition(1.0, 0, 0); + ignition::math::Vector3d particleSize(0.2, 0.2, 0.2); + ignition::rendering::ParticleEmitterPtr emitter = + scene->CreateParticleEmitter(); + emitter->SetLocalPosition(particlePosition); + emitter->SetParticleSize(particleSize); + emitter->SetRate(100); + emitter->SetLifetime(2); + emitter->SetVelocityRange(0.1, 0.1); + emitter->SetScaleRate(0.2); + emitter->SetColorRange(ignition::math::Color::Red, + ignition::math::Color::Black); + emitter->SetEmitting(true); + root->AddChild(emitter); + + // Verify rays caster 1 range readings + // listen to new gpu rays frames + unsigned int channels = gpuRays->Channels(); + float *scan = new float[hRayCount * vRayCount * channels]; + common::ConnectionPtr c = + gpuRays->ConnectNewGpuRaysFrame( + std::bind(&::OnNewGpuRaysFrame, scan, + std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, + std::placeholders::_4, std::placeholders::_5)); + + + int mid = static_cast(hRayCount / 2) * channels; + int last = (hRayCount - 1) * channels; + double unitBoxSize = 1.0; + double expectedRangeAtMidPointBox1 = + abs(box01Pose.Pos().X()) - unitBoxSize / 2; + double expectedRangeAtMidPointBox2 = + abs(box02Pose.Pos().Y()) - unitBoxSize / 2; + + // set a larger tol for particle range + double laserNoiseTol = particleSize.X(); + double expectedParticleRange = particlePosition.X(); + + // update 100 frames. There should be a descent chance that we will see both + // a particle hit and miss in the readings returned by the sensor + unsigned int particleHitCount = 0u; + unsigned int particleMissCount = 0u; + for (unsigned int i = 0u; i < 100u; ++i) + { + gpuRays->Update(); + + // sensor should see ether a particle or box01 + double particleRange = static_cast(scan[mid]); + bool particleHit = ignition::math::equal( + expectedParticleRange, particleRange, laserNoiseTol); + bool particleMiss = ignition::math::equal( + expectedRangeAtMidPointBox1, particleRange, LASER_TOL); + EXPECT_TRUE(particleHit || particleMiss) + << "actual vs expected particle range: " + << particleRange << " vs " << expectedParticleRange; + + particleHitCount += particleHit ? 1u : 0u; + particleMissCount += particleMiss ? 1u : 0u; + + // sensor should see box02 without noise or scatter effect + EXPECT_NEAR(expectedRangeAtMidPointBox2, scan[0], LASER_TOL); + + // sensor should not see box03 as it is out of range + EXPECT_DOUBLE_EQ(ignition::math::INF_D, scan[last]); + } + + // particles are sparse so there should be more misses than hits + EXPECT_GT(particleMissCount, particleHitCount); + // there should be at least one hit + EXPECT_GT(particleHitCount, 0u); + + c.reset(); + + delete [] scan; + + scan = nullptr; + + // Clean up + engine->DestroyScene(scene); + rendering::unloadEngine(engine->Name()); +} ///////////////////////////////////////////////// TEST_P(GpuRaysTest, Configure) { @@ -490,6 +661,12 @@ TEST_P(GpuRaysTest, LaserVertical) LaserVertical(GetParam()); } +///////////////////////////////////////////////// +TEST_P(GpuRaysTest, RaysParticles) +{ + RaysParticles(GetParam()); +} + INSTANTIATE_TEST_CASE_P(GpuRays, GpuRaysTest, RENDER_ENGINE_VALUES, ignition::rendering::PrintToStringParam()); diff --git a/test/integration/thermal_camera.cc b/test/integration/thermal_camera.cc index f3e52dcda..dc7144cf3 100644 --- a/test/integration/thermal_camera.cc +++ b/test/integration/thermal_camera.cc @@ -21,8 +21,11 @@ #include #include +#include + #include "test_config.h" // NOLINT(build/include) +#include "ignition/rendering/ParticleEmitter.hh" #include "ignition/rendering/PixelFormat.hh" #include "ignition/rendering/RenderEngine.hh" #include "ignition/rendering/RenderingIface.hh" @@ -63,6 +66,9 @@ class ThermalCameraTest: public testing::Test, // Test 8 bit thermal camera output public: void ThermalCameraBoxes8Bit(const std::string &_renderEngine); + // Test that particles do not appear in thermal camera image + public: void ThermalCameraParticles(const std::string &_renderEngine); + // Path to test textures public: const std::string TEST_MEDIA_PATH = ignition::common::joinPaths(std::string(PROJECT_SOURCE_PATH), @@ -448,6 +454,176 @@ void ThermalCameraTest::ThermalCameraBoxes8Bit( ignition::rendering::unloadEngine(engine->Name()); } +////////////////////////////////////////////////// +void ThermalCameraTest::ThermalCameraParticles( + const std::string &_renderEngine) +{ + int imgWidth = 50; + int imgHeight = 50; + double aspectRatio = imgWidth / imgHeight; + + double unitBoxSize = 1.0; + ignition::math::Vector3d boxPosition(1.8, 0.0, 0.0); + + // Only ogre2 supports 8 bit image format + if (_renderEngine.compare("ogre2") != 0) + { + igndbg << "Engine '" << _renderEngine + << "' doesn't support 8 bit thermal cameras" << std::endl; + return; + } + + // Setup ign-rendering with an empty scene + auto *engine = ignition::rendering::engine(_renderEngine); + if (!engine) + { + igndbg << "Engine '" << _renderEngine + << "' is not supported" << std::endl; + return; + } + + ignition::rendering::ScenePtr scene = engine->CreateScene("scene"); + + // red background + scene->SetBackgroundColor(1.0, 0.0, 0.0); + + // Create an scene with a box in it + scene->SetAmbientLight(1.0, 1.0, 1.0); + ignition::rendering::VisualPtr root = scene->RootVisual(); + + // create box visual + ignition::rendering::VisualPtr box = scene->CreateVisual(); + box->AddGeometry(scene->CreateBox()); + box->SetOrigin(0.0, 0.0, 0.0); + box->SetLocalPosition(boxPosition); + box->SetLocalRotation(0, 0, 0); + box->SetLocalScale(unitBoxSize, unitBoxSize, unitBoxSize); + + // set box temperature + float boxTemp = 310.0; + box->SetUserData("temperature", boxTemp); + + root->AddChild(box); + + // create particle emitter between camera and box + ignition::rendering::ParticleEmitterPtr emitter = + scene->CreateParticleEmitter(); + emitter->SetLocalPosition({0.5, 0, 0}); + emitter->SetRate(10); + emitter->SetParticleSize({1, 1, 1}); + emitter->SetLifetime(2); + emitter->SetVelocityRange(0.1, 0.5); + emitter->SetColorRange(ignition::math::Color::Red, + ignition::math::Color::Black); + emitter->SetScaleRate(1); + emitter->SetEmitting(true); + + root->AddChild(emitter); + + { + double farDist = 10.0; + double nearDist = 0.15; + double hfov = 1.05; + // set min max values based on thermal camera spec + // using the Vividia HTi HT-301 camera as example: + // https://hti-instrument.com/products/ht-301-mobile-phone-thermal-imager + // The range is ~= -20 to 400 degree Celsius + double minTemp = 253.0; + double maxTemp = 673.0; + // Create thermal camera + auto thermalCamera = scene->CreateThermalCamera("ThermalCamera"); + ASSERT_NE(thermalCamera, nullptr); + + ignition::math::Pose3d testPose(ignition::math::Vector3d(0, 0, 0), + ignition::math::Quaterniond::Identity); + thermalCamera->SetLocalPose(testPose); + + // Configure thermal camera + thermalCamera->SetImageWidth(imgWidth); + EXPECT_EQ(thermalCamera->ImageWidth(), + static_cast(imgWidth)); + thermalCamera->SetImageHeight(imgHeight); + EXPECT_EQ(thermalCamera->ImageHeight(), + static_cast(imgHeight)); + thermalCamera->SetFarClipPlane(farDist); + EXPECT_DOUBLE_EQ(thermalCamera->FarClipPlane(), farDist); + thermalCamera->SetNearClipPlane(nearDist); + EXPECT_DOUBLE_EQ(thermalCamera->NearClipPlane(), nearDist); + thermalCamera->SetAspectRatio(aspectRatio); + EXPECT_DOUBLE_EQ(thermalCamera->AspectRatio(), aspectRatio); + thermalCamera->SetHFOV(hfov); + EXPECT_DOUBLE_EQ(thermalCamera->HFOV().Radian(), hfov); + + // set bit depth + thermalCamera->SetImageFormat(ignition::rendering::PF_L8); + EXPECT_EQ(ignition::rendering::PF_L8, thermalCamera->ImageFormat()); + + // set min max temp + thermalCamera->SetMinTemperature(minTemp); + EXPECT_DOUBLE_EQ(minTemp, thermalCamera->MinTemperature()); + thermalCamera->SetMaxTemperature(maxTemp); + EXPECT_DOUBLE_EQ(maxTemp, thermalCamera->MaxTemperature()); + + // thermal-specific params + // set room temperature: 294 ~ 298 Kelvin + float ambientTemp = 296.0f; + float ambientTempRange = 4.0f; + + // 8 bit format so higher number here (lower resolution) + // +- 3 degrees + float linearResolution = 3.0f; + thermalCamera->SetAmbientTemperature(ambientTemp); + EXPECT_FLOAT_EQ(ambientTemp, thermalCamera->AmbientTemperature()); + thermalCamera->SetAmbientTemperatureRange(ambientTempRange); + EXPECT_FLOAT_EQ(ambientTempRange, thermalCamera->AmbientTemperatureRange()); + thermalCamera->SetLinearResolution(linearResolution); + EXPECT_FLOAT_EQ(linearResolution, thermalCamera->LinearResolution()); + scene->RootVisual()->AddChild(thermalCamera); + + // Set a callback on the camera sensor to get a thermal camera frame + // todo(anyone) change this to uint8_t when thermal cameras supports a + // ConnectNewThermalFrame event that provides this format + uint16_t *thermalData = new uint16_t[imgHeight * imgWidth]; + ignition::common::ConnectionPtr connection = + thermalCamera->ConnectNewThermalFrame( + std::bind(&::OnNewThermalFrame, thermalData, + std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, + std::placeholders::_4, std::placeholders::_5)); + EXPECT_NE(nullptr, connection); + + // thermal image indices + int midWidth = static_cast(thermalCamera->ImageWidth() * 0.5); + int midHeight = static_cast(thermalCamera->ImageHeight() * 0.5); + int mid = midHeight * thermalCamera->ImageWidth() + midWidth -1; + int left = midHeight * thermalCamera->ImageWidth(); + int right = (midHeight+1) * thermalCamera->ImageWidth() - 1; + + // Update a few times to make sure the flow of particles do not affect + // the readings + for (unsigned int i = 0; i < 100u; ++i) + { + thermalCamera->Update(); + + // verify temperature + // Box should be in the middle of image and return box temp + // Left and right side of the image frame should be ambient temp + EXPECT_NEAR(ambientTemp, thermalData[left] * linearResolution, + ambientTempRange); + EXPECT_NEAR(ambientTemp, thermalData[right] * linearResolution, + ambientTempRange); + EXPECT_FLOAT_EQ(thermalData[right], thermalData[left]); + EXPECT_NEAR(boxTemp, thermalData[mid] * linearResolution, + linearResolution); + } + + // Clean up + connection.reset(); + delete [] thermalData; + } + + engine->DestroyScene(scene); + ignition::rendering::unloadEngine(engine->Name()); +} TEST_P(ThermalCameraTest, ThermalCameraBoxesUniformTemp) { @@ -464,6 +640,11 @@ TEST_P(ThermalCameraTest, ThermalCameraBoxesUniformTemp8Bit) ThermalCameraBoxes8Bit(GetParam()); } +TEST_P(ThermalCameraTest, ThermalCameraParticles) +{ + ThermalCameraParticles(GetParam()); +} + INSTANTIATE_TEST_CASE_P(ThermalCamera, ThermalCameraTest, RENDER_ENGINE_VALUES, ignition::rendering::PrintToStringParam());