diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 9fa3ba7da..7bdc02251 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -5,7 +5,7 @@ labels: bug --- +https://robotics.stackexchange.com instead.--> ## Environment * OS Version: diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 87233a479..52b56e336 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -6,7 +6,7 @@ labels: enhancement +https://robotics.stackexchange.com instead.--> ## Desired behavior diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d6cd9b212..a8b59821d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,12 @@ name: Ubuntu CI -on: [push, pull_request] +on: + pull_request: + push: + branches: + - 'ign-rendering[0-9]' + - 'gz-rendering[0-9]?' + - 'main' jobs: jammy-ci: diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml index 2c94852da..2332244bf 100644 --- a/.github/workflows/triage.yml +++ b/.github/workflows/triage.yml @@ -14,4 +14,3 @@ jobs: with: project-url: https://github.com/orgs/gazebosim/projects/7 github-token: ${{ secrets.TRIAGE_TOKEN }} - diff --git a/Changelog.md b/Changelog.md index e581e647f..5df59a721 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1403,6 +1403,16 @@ 1. Add support for transparency based on textures alpha channel for ogre1 and ogre2 * [BitBucket pull request 229](https://osrf-migration.github.io/ignition-gh-pages/#!/ignitionrobotics/ign-rendering/pull-requests/229) +### Gazebo Rendering 3.7.2 (2024-01-05) + +1. Update github action workflows + * [Pull request #940](https://github.com/gazebosim/gz-rendering/pull/940) + * [Pull request #834](https://github.com/gazebosim/gz-rendering/pull/834) + * [Pull request #833](https://github.com/gazebosim/gz-rendering/pull/833) + +1. Add minor comments to BaseGizmoVisual + * [Pull request #881](https://github.com/gazebosim/gz-rendering/pull/881) + ### Gazebo Rendering 3.7.1 (2023-02-03) 1. Remove fini to resolve segfault at shutdown. diff --git a/include/gz/rendering/base/BaseGizmoVisual.hh b/include/gz/rendering/base/BaseGizmoVisual.hh index 286cc66ac..f35954d5c 100644 --- a/include/gz/rendering/base/BaseGizmoVisual.hh +++ b/include/gz/rendering/base/BaseGizmoVisual.hh @@ -499,6 +499,8 @@ namespace gz this->visuals[TransformAxis::TA_TRANSLATION_X] = transXVis; this->visuals[TransformAxis::TA_TRANSLATION_Y] = transYVis; this->visuals[TransformAxis::TA_TRANSLATION_Z] = transZVis; + // Store the translation origin visual in this->visuals using a key + // that's not already occupied by the TransformAxis enum this->visuals[TransformAxis::TA_TRANSLATION_Z << 1] = transOrigin; // translation handles @@ -588,6 +590,8 @@ namespace gz this->visuals[TransformAxis::TA_ROTATION_X] = rotXVis; this->visuals[TransformAxis::TA_ROTATION_Y] = rotYVis; this->visuals[TransformAxis::TA_ROTATION_Z] = rotZVis; + // Store the full rotation visual in this->visuals using a key + // that's not already occupied by the TransformAxis enum this->visuals[TransformAxis::TA_ROTATION_Z << 1] = rotFullVis; // rotation handles diff --git a/ogre2/src/Ogre2DepthCamera.cc b/ogre2/src/Ogre2DepthCamera.cc index d1b0f5a44..6d182dbd8 100644 --- a/ogre2/src/Ogre2DepthCamera.cc +++ b/ogre2/src/Ogre2DepthCamera.cc @@ -23,6 +23,7 @@ #include #endif +#include #include #include #include @@ -31,7 +32,6 @@ #include "gz/rendering/ogre2/Ogre2Conversions.hh" #include "gz/rendering/ogre2/Ogre2DepthCamera.hh" #include "gz/rendering/ogre2/Ogre2GaussianNoisePass.hh" -#include "gz/rendering/ogre2/Ogre2Includes.hh" #include "gz/rendering/ogre2/Ogre2ParticleEmitter.hh" #include "gz/rendering/ogre2/Ogre2RenderEngine.hh" #include "gz/rendering/ogre2/Ogre2RenderTarget.hh" @@ -41,6 +41,21 @@ #include "Ogre2ParticleNoiseListener.hh" +#ifdef _MSC_VER + #pragma warning(push, 0) +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _MSC_VER + #pragma warning(pop) +#endif + namespace gz { namespace rendering @@ -148,6 +163,19 @@ class gz::rendering::Ogre2DepthCameraPrivate /// \brief Name of shadow compositor node public: const std::string kShadowNodeName = "PbsMaterialsShadowNode"; + + /// \brief Execution mask for this workspace + /// If RGB point color data are requested, the execution mask of the color + /// target will be updated to match the workspace's execution mask so that + /// these passes are executed, otherwise they will be skipped for performance + /// improvement. + public: const uint8_t kDepthExecutionMask = 0xEF; + + /// \brief Pointer to the color target definition in the workspace + public: Ogre::CompositorTargetDef *colorTargetDef{nullptr}; + + /// \brief Pointer to the particle target definition in the workspace + public: Ogre::CompositorTargetDef *particleTargetDef{nullptr}; }; using namespace gz; @@ -315,6 +343,8 @@ void Ogre2DepthCamera::Destroy() { ogreCompMgr->removeWorkspace( this->dataPtr->ogreCompositorWorkspace); + this->dataPtr->colorTargetDef = nullptr; + this->dataPtr->particleTargetDef = nullptr; } if (this->dataPtr->depthMaterial) @@ -702,19 +732,24 @@ void Ogre2DepthCamera::CreateDepthTexture() rtvParticleTexture->depthAttachment.textureName = "particleDepthTexture"; baseNodeDef->setNumTargetPass(4); - Ogre::CompositorTargetDef *colorTargetDef = + this->dataPtr->colorTargetDef = baseNodeDef->addTargetPass("colorTexture"); - if (validBackground) - colorTargetDef->setNumPasses(3); + this->dataPtr->colorTargetDef->setNumPasses(4); else - colorTargetDef->setNumPasses(2); + this->dataPtr->colorTargetDef->setNumPasses(3); { + // clear pass + Ogre::CompositorPassClearDef *passClear = + static_cast( + this->dataPtr->colorTargetDef->addPass(Ogre::PASS_CLEAR)); + passClear->mExecutionMask = this->dataPtr->kDepthExecutionMask; + // scene pass - opaque { Ogre::CompositorPassSceneDef *passScene = static_cast( - colorTargetDef->addPass(Ogre::PASS_SCENE)); + this->dataPtr->colorTargetDef->addPass(Ogre::PASS_SCENE)); passScene->mShadowNode = this->dataPtr->kShadowNodeName; passScene->setVisibilityMask(GZ_VISIBILITY_ALL); passScene->mIncludeOverlays = false; @@ -733,6 +768,7 @@ void Ogre2DepthCamera::CreateDepthTexture() Ogre2Conversions::Convert(this->Scene()->BackgroundColor())); } + passScene->mExecutionMask = ~this->dataPtr->kDepthExecutionMask; } // render background, e.g. sky, after opaque stuff @@ -741,23 +777,25 @@ void Ogre2DepthCamera::CreateDepthTexture() // quad pass Ogre::CompositorPassQuadDef *passQuad = static_cast( - colorTargetDef->addPass(Ogre::PASS_QUAD)); + this->dataPtr->colorTargetDef->addPass(Ogre::PASS_QUAD)); passQuad->mMaterialName = this->dataPtr->kSkyboxMaterialName + "_" + this->Name(); passQuad->mFrustumCorners = Ogre::CompositorPassQuadDef::CAMERA_DIRECTION; + passQuad->mExecutionMask = ~this->dataPtr->kDepthExecutionMask; } // scene pass - transparent stuff { Ogre::CompositorPassSceneDef *passScene = static_cast( - colorTargetDef->addPass(Ogre::PASS_SCENE)); + this->dataPtr->colorTargetDef->addPass(Ogre::PASS_SCENE)); passScene->setVisibilityMask(GZ_VISIBILITY_ALL); // todo(anyone) PbsMaterialsShadowNode is hardcoded. // Although this may be just fine passScene->mShadowNode = this->dataPtr->kShadowNodeName; passScene->mFirstRQ = 2u; + passScene->mExecutionMask = ~this->dataPtr->kDepthExecutionMask; } } @@ -774,23 +812,36 @@ void Ogre2DepthCamera::CreateDepthTexture() this->FarClipPlane(), this->FarClipPlane(), this->FarClipPlane())); - // depth texute does not contain particles + // depth texture does not contain particles passScene->setVisibilityMask( GZ_VISIBILITY_ALL & ~Ogre2ParticleEmitter::kParticleVisibilityFlags); + passScene->mEnableForwardPlus = false; + passScene->setLightVisibilityMask(0x0); } - Ogre::CompositorTargetDef *particleTargetDef = + // Ogre::CompositorTargetDef *particleTargetDef = + this->dataPtr->particleTargetDef = baseNodeDef->addTargetPass("particleTexture"); - particleTargetDef->setNumPasses(1); + this->dataPtr->particleTargetDef->setNumPasses(2); { + // clear pass + Ogre::CompositorPassClearDef *passClear = + static_cast( + this->dataPtr->particleTargetDef->addPass(Ogre::PASS_CLEAR)); + passClear->setAllClearColours(Ogre::ColourValue::Black); + passClear->mExecutionMask = this->dataPtr->kDepthExecutionMask; + // scene pass Ogre::CompositorPassSceneDef *passScene = static_cast( - particleTargetDef->addPass(Ogre::PASS_SCENE)); + this->dataPtr->particleTargetDef->addPass(Ogre::PASS_SCENE)); passScene->setAllLoadActions(Ogre::LoadAction::Clear); passScene->setAllClearColours(Ogre::ColourValue::Black); passScene->setVisibilityMask( Ogre2ParticleEmitter::kParticleVisibilityFlags); + passScene->mEnableForwardPlus = false; + passScene->setLightVisibilityMask(0x0); + passScene->mExecutionMask = ~this->dataPtr->kDepthExecutionMask; } // rt0 target - converts depth to xyz @@ -920,7 +971,7 @@ void Ogre2DepthCamera::CreateDepthTexture() Ogre::GpuResidency::Resident); } - CreateWorkspaceInstance(); + this->CreateWorkspaceInstance(); } ////////////////////////////////////////////////// @@ -942,7 +993,8 @@ void Ogre2DepthCamera::CreateWorkspaceInstance() externalTargets, this->ogreCamera, this->dataPtr->ogreCompositorWorkspaceDef, - false); + false, -1, 0, 0, Ogre::Vector4::ZERO, 0x00, + this->dataPtr->kDepthExecutionMask); this->dataPtr->ogreCompositorWorkspace->addListener( engine->TerraWorkspaceListener()); @@ -1017,6 +1069,51 @@ void Ogre2DepthCamera::PreRender() if (!this->dataPtr->ogreCompositorWorkspace) this->CreateWorkspaceInstance(); + + // Disable color target (set to clear pass) if there are no rgb point cloud + // connections + if (this->dataPtr->colorTargetDef) + { + Ogre::CompositorPassDefVec &colorPasses = + this->dataPtr->colorTargetDef->getCompositorPassesNonConst(); + GZ_ASSERT(colorPasses.size() > 2u, + "Ogre2DepthCamera color target should contain more than 2 passes"); + GZ_ASSERT(colorPasses[0]->getType() == Ogre::PASS_CLEAR, + "Ogre2DepthCamera color target should start with a clear pass"); + colorPasses[0]->mExecutionMask = + (this->dataPtr->newRgbPointCloud.ConnectionCount() > 0u) ? + ~this->dataPtr->kDepthExecutionMask :this->dataPtr->kDepthExecutionMask; + for (unsigned int i = 1; i < colorPasses.size(); ++i) + { + colorPasses[i]->mExecutionMask = + (this->dataPtr->newRgbPointCloud.ConnectionCount() > 0u) ? + this->dataPtr->kDepthExecutionMask : + ~this->dataPtr->kDepthExecutionMask; + } + } + + // Disable particle target (set to clear pass) if there are no particles + if (this->dataPtr->particleTargetDef) + { + bool hasParticles = + this->scene->OgreSceneManager()->getMovableObjectIterator( + Ogre::ParticleSystemFactory::FACTORY_TYPE_NAME).hasMoreElements(); + Ogre::CompositorPassDefVec &particlePasses = + this->dataPtr->particleTargetDef->getCompositorPassesNonConst(); + GZ_ASSERT(particlePasses.size() == 2u, + "Ogre2DepthCamera particle target should 2 passes"); + GZ_ASSERT(particlePasses[0]->getType() == Ogre::PASS_CLEAR, + "Ogre2DepthCamera particle target should start with a clear pass"); + GZ_ASSERT(particlePasses[1]->getType() == Ogre::PASS_SCENE, + "Ogre2DepthCamera particle target should end with a scene pass"); + particlePasses[0]->mExecutionMask = + (hasParticles) ? ~this->dataPtr->kDepthExecutionMask : + this->dataPtr->kDepthExecutionMask; + particlePasses[1]->mExecutionMask = + (hasParticles) ? this->dataPtr->kDepthExecutionMask : + ~this->dataPtr->kDepthExecutionMask; + } + // update depth camera render passes Ogre2RenderTarget::UpdateRenderPassChain( this->dataPtr->ogreCompositorWorkspace, diff --git a/ogre2/src/Ogre2GlobalIlluminationCiVct.cc b/ogre2/src/Ogre2GlobalIlluminationCiVct.cc index 2a90fac9c..553451964 100644 --- a/ogre2/src/Ogre2GlobalIlluminationCiVct.cc +++ b/ogre2/src/Ogre2GlobalIlluminationCiVct.cc @@ -40,7 +40,7 @@ using namespace gz; using namespace rendering; /// \brief Private data for the Ogre2CiVctCascadePrivate class -class DETAIL_GZ_RENDERING_OGRE2_HIDDEN gz::rendering::Ogre2CiVctCascadePrivate +class GZ_RENDERING_OGRE2_HIDDEN gz::rendering::Ogre2CiVctCascadePrivate { // clang-format off /// \brief Pointer to cascade setting @@ -49,7 +49,7 @@ class DETAIL_GZ_RENDERING_OGRE2_HIDDEN gz::rendering::Ogre2CiVctCascadePrivate }; /// \brief Private data for the Ogre2GlobalIlluminationCiVct class -class DETAIL_GZ_RENDERING_OGRE2_HIDDEN +class GZ_RENDERING_OGRE2_HIDDEN gz::rendering::Ogre2GlobalIlluminationCiVctPrivate { // clang-format off diff --git a/ogre2/src/Ogre2GpuRays.cc b/ogre2/src/Ogre2GpuRays.cc index 9dc58e9b8..48e1dc5f6 100644 --- a/ogre2/src/Ogre2GpuRays.cc +++ b/ogre2/src/Ogre2GpuRays.cc @@ -205,6 +205,16 @@ class GZ_RENDERING_OGRE2_HIDDEN gz::rendering::Ogre2GpuRaysPrivate /// \brief Max number of cameras used for creating the cubemap of depth /// textures for generating lidar data public: const unsigned int kCubeCameraCount = 6; + + /// \brief Execution mask for this workspace + /// If particles exist in the scene, the execution mask of the particle + /// target will be updated to match the workspace's execution mask so that + /// these passes are executed, otherwise they will be skipped for performance + /// improvement. + public: const uint8_t kGpuRaysExecutionMask = 0xEF; + + /// \brief Pointer to the particle target definition in the workspace + public: Ogre::CompositorTargetDef *particleTargetDef{nullptr}; }; using namespace gz; @@ -621,6 +631,7 @@ void Ogre2GpuRays::Destroy() this->dataPtr->firstPassTextures[i] = nullptr; } } + this->dataPtr->particleTargetDef = nullptr; // remove 2nd pass texture, material, compositor if (this->dataPtr->secondPassTexture) @@ -1021,12 +1032,18 @@ void Ogre2GpuRays::Setup1stPass() Ogre::CompositorNodeDef *nodeDef = ogreCompMgr->getNodeDefinitionNonConst("GpuRays1stPass"); Ogre::CompositorTargetDef *target0 = nodeDef->getTargetPass(0); - Ogre::CompositorPassDefVec &passes = target0->getCompositorPassesNonConst(); - assert(passes.size() >= 1u); - assert(passes[0]->getType() == Ogre::PASS_SCENE); - assert(dynamic_cast(passes[0])); + Ogre::CompositorPassDefVec &colorPasses = + target0->getCompositorPassesNonConst(); + assert(colorPasses.size() >= 1u); + assert(colorPasses[0]->getType() == Ogre::PASS_SCENE); + assert(dynamic_cast(colorPasses[0])); this->dataPtr->mainPassSceneDef = - static_cast(passes[0]); + static_cast(colorPasses[0]); + + Ogre::CompositorTargetDef *target1 = nodeDef->getTargetPass(1); + this->dataPtr->particleTargetDef = target1; + GZ_ASSERT(this->dataPtr->particleTargetDef != nullptr, + "Unable to get particle target in Ogre2GpuRays"); const std::string wsDefName = "GpuRays1stPassWorkspace"; Ogre::CompositorWorkspaceDef *wsDef = @@ -1096,7 +1113,8 @@ void Ogre2GpuRays::Setup1stPass() compoChannels, this->dataPtr->cubeCam[i], wsDefName, - false); + false, -1, 0, 0, Ogre::Vector4::ZERO, 0x00, + this->dataPtr->kGpuRaysExecutionMask); compoChannels.pop_back(); @@ -1271,6 +1289,27 @@ void Ogre2GpuRays::PreRender() { if (!this->dataPtr->cubeUVTexture) this->CreateGpuRaysTextures(); + + if (this->dataPtr->particleTargetDef) + { + bool hasParticles = + this->scene->OgreSceneManager()->getMovableObjectIterator( + Ogre::ParticleSystemFactory::FACTORY_TYPE_NAME).hasMoreElements(); + Ogre::CompositorPassDefVec &particlePasses = + this->dataPtr->particleTargetDef->getCompositorPassesNonConst(); + GZ_ASSERT(particlePasses.size() == 2u, + "Ogre2DepthCamera particle target should 2 passes"); + GZ_ASSERT(particlePasses[0]->getType() == Ogre::PASS_CLEAR, + "Ogre2DepthCamera particle target should start with a clear pass"); + GZ_ASSERT(particlePasses[1]->getType() == Ogre::PASS_SCENE, + "Ogre2DepthCamera particle target should end with a scene pass"); + particlePasses[0]->mExecutionMask = + (hasParticles) ? ~this->dataPtr->kGpuRaysExecutionMask : + this->dataPtr->kGpuRaysExecutionMask; + particlePasses[1]->mExecutionMask = + (hasParticles) ? this->dataPtr->kGpuRaysExecutionMask : + ~this->dataPtr->kGpuRaysExecutionMask; + } } ////////////////////////////////////////////////// diff --git a/ogre2/src/Ogre2MeshFactory.cc b/ogre2/src/Ogre2MeshFactory.cc index 71f25723c..54bc8e20e 100644 --- a/ogre2/src/Ogre2MeshFactory.cc +++ b/ogre2/src/Ogre2MeshFactory.cc @@ -381,14 +381,29 @@ bool Ogre2MeshFactory::LoadImpl(const MeshDescriptor &_desc) // TODO(anyone): specular colors // two dimensional texture coordinates - // add all texture coordinate sets - for (unsigned int k = 0u; k < subMesh.TexCoordSetCount(); ++k) + // If submesh does not have texcoord sets, add one default set. + // This is needed otherwise ogre2 will fail to generate tangents during + // Ogre::v2::Mesh::importV1() and throw an ogre exception if we try to + // apply normal maps to a submesh. Resulting object would then appear + // white without any PBR textures. + if (subMesh.TexCoordSetCount() == 0u) { - if (subMesh.TexCoordCountBySet(k) > 0) + vertexDecl->addElement(0, currOffset, Ogre::VET_FLOAT2, + Ogre::VES_TEXTURE_COORDINATES, 0); + currOffset += Ogre::v1::VertexElement::getTypeSize(Ogre::VET_FLOAT2); + } + // Add all texture coordinate sets + else + { + for (unsigned int k = 0u; k < subMesh.TexCoordSetCount(); ++k) { - vertexDecl->addElement(0, currOffset, Ogre::VET_FLOAT2, - Ogre::VES_TEXTURE_COORDINATES, k); - currOffset += Ogre::v1::VertexElement::getTypeSize(Ogre::VET_FLOAT2); + if (subMesh.TexCoordCountBySet(k) > 0) + { + vertexDecl->addElement(0, currOffset, Ogre::VET_FLOAT2, + Ogre::VES_TEXTURE_COORDINATES, k); + currOffset += + Ogre::v1::VertexElement::getTypeSize(Ogre::VET_FLOAT2); + } } } @@ -436,12 +451,20 @@ bool Ogre2MeshFactory::LoadImpl(const MeshDescriptor &_desc) } // Add all texture coordinate sets - for (unsigned int k = 0u; k < subMesh.TexCoordSetCount(); ++k) + if (subMesh.TexCoordSetCount() == 0u) + { + *vertices++ = 0; + *vertices++ = 0; + } + else { - if (subMesh.TexCoordCountBySet(k) > 0u) + for (unsigned int k = 0u; k < subMesh.TexCoordSetCount(); ++k) { - *vertices++ = subMesh.TexCoordBySet(j, k).X(); - *vertices++ = subMesh.TexCoordBySet(j, k).Y(); + if (subMesh.TexCoordCountBySet(k) > 0u) + { + *vertices++ = subMesh.TexCoordBySet(j, k).X(); + *vertices++ = subMesh.TexCoordBySet(j, k).Y(); + } } } } diff --git a/ogre2/src/media/materials/scripts/GpuRays.compositor b/ogre2/src/media/materials/scripts/GpuRays.compositor index c3d2417e0..1008934ca 100644 --- a/ogre2/src/media/materials/scripts/GpuRays.compositor +++ b/ogre2/src/media/materials/scripts/GpuRays.compositor @@ -45,6 +45,12 @@ compositor_node GpuRays1stPass target particleTextureRtv { + pass clear + { + execution_mask 0xEF + colour_value 0 0 0 1 + } + pass render_scene { load @@ -59,6 +65,8 @@ compositor_node GpuRays1stPass enable_forwardplus no light_visibility_mask 0x0 + execution_mask 0x0 + profiling_id "GpuRays1stPass Particle" } } diff --git a/test/integration/CMakeLists.txt b/test/integration/CMakeLists.txt index f6e7b4ede..646b4c84c 100644 --- a/test/integration/CMakeLists.txt +++ b/test/integration/CMakeLists.txt @@ -7,6 +7,7 @@ set(tests gpu_rays heightmap lidar_visual + mesh projector render_pass scene diff --git a/test/integration/mesh.cc b/test/integration/mesh.cc new file mode 100644 index 000000000..59e361fd7 --- /dev/null +++ b/test/integration/mesh.cc @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2024 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +#include + +#include +#include + +#include "CommonRenderingTest.hh" + +#include +#include +#include +#include + +#include "gz/rendering/Camera.hh" +#include "gz/rendering/Image.hh" +#include "gz/rendering/PixelFormat.hh" +#include "gz/rendering/Scene.hh" + +using namespace gz; +using namespace rendering; + +class MeshTest: public CommonRenderingTest +{ +}; + +///////////////////////////////////////////////// +TEST_F(MeshTest, NormalMapWithoutTexCoord) +{ + // Create a mesh with 2 submeshes - one with red texture and the other with + // green texture. Add texcoords to the red submesh but not the green submesh. + // Verify that we can set normal map to the red submesh and the two + // submeshes should be rendered correctly + + ScenePtr scene = engine->CreateScene("scene"); + ASSERT_NE(nullptr, scene); + scene->SetAmbientLight(1.0, 1.0, 1.0); + scene->SetBackgroundColor(0.0, 0.0, 1.0); + + VisualPtr root = scene->RootVisual(); + ASSERT_NE(nullptr, root); + + // create directional light + DirectionalLightPtr light0 = scene->CreateDirectionalLight(); + light0->SetDirection(0.0, 0.0, -1); + light0->SetDiffuseColor(1.0, 1.0, 1.0); + light0->SetSpecularColor(1.0, 1.0, 1.0); + root->AddChild(light0); + + common::Mesh mesh; + common::SubMesh subMesh0; + subMesh0.SetName("submesh0"); + subMesh0.AddVertex(math::Vector3d(0, 0, 0)); + subMesh0.AddVertex(math::Vector3d(1, 0, 0)); + subMesh0.AddVertex(math::Vector3d(1, 1, 0)); + subMesh0.AddNormal(math::Vector3d(0, 0, 1)); + subMesh0.AddNormal(math::Vector3d(0, 0, 1)); + subMesh0.AddNormal(math::Vector3d(0, 0, 1)); + subMesh0.AddIndex(0); + subMesh0.AddIndex(1); + subMesh0.AddIndex(2); + subMesh0.AddTexCoordBySet(math::Vector2d(0, 0), 0); + subMesh0.AddTexCoordBySet(math::Vector2d(0, 1), 0); + subMesh0.AddTexCoordBySet(math::Vector2d(0, 0), 0); + common::MaterialPtr material0; + material0 = std::make_shared(); + std::string textureMapName0 = "red_diffuse_map"; + auto textureMapData0 = std::make_shared(); + auto red = std::make_unique(3); + red.get()[0] = 255u; + red.get()[1] = 0u; + red.get()[2] = 0u; + textureMapData0->SetFromData(red.get(), 1, 1, common::Image::RGB_INT8); + material0->SetTextureImage(textureMapName0, textureMapData0); + + common::Pbr pbr; + std::string normalMapName = "normal_map"; + auto normalMapData = std::make_shared(); + auto normal = std::make_unique(3); + normal.get()[0] = 127u; + normal.get()[1] = 127u; + normal.get()[2] = 255u; + normalMapData->SetFromData(normal.get(), 1, 1, common::Image::RGB_INT8); + + pbr.SetNormalMap(normalMapName, common::NormalMapSpace::TANGENT, + normalMapData); + material0->SetPbrMaterial(pbr); + mesh.AddMaterial(material0); + subMesh0.SetMaterialIndex(0u); + + common::SubMesh subMesh1; + subMesh1.SetName("submesh1"); + subMesh1.AddVertex(math::Vector3d(0, 0, 0)); + subMesh1.AddVertex(math::Vector3d(1, 1, 0)); + subMesh1.AddVertex(math::Vector3d(0, 1, 0)); + subMesh1.AddNormal(math::Vector3d(0, 0, 1)); + subMesh1.AddNormal(math::Vector3d(0, 0, 1)); + subMesh1.AddNormal(math::Vector3d(0, 0, 1)); + subMesh1.AddIndex(0); + subMesh1.AddIndex(1); + subMesh1.AddIndex(2); + + common::MaterialPtr material1; + material1 = std::make_shared(); + std::string textureMapName1 = "green_diffuse_map"; + auto textureMapData1 = std::make_shared(); + auto green = std::make_unique(3); + green.get()[0] = 0u; + green.get()[1] = 255u; + green.get()[2] = 0u; + textureMapData1->SetFromData(green.get(), 1, 1, common::Image::RGB_INT8); + material1->SetTextureImage(textureMapName1, textureMapData1); + mesh.AddMaterial(material1); + subMesh1.SetMaterialIndex(1u); + + mesh.AddSubMesh(subMesh0); + mesh.AddSubMesh(subMesh1); + + MeshDescriptor descriptor; + descriptor.meshName = "test_mesh"; + descriptor.mesh = &mesh; + MeshPtr meshGeom = scene->CreateMesh(descriptor); + + VisualPtr visual = scene->CreateVisual("visual"); + visual->AddGeometry(meshGeom); + root->AddChild(visual); + + // create camera + CameraPtr camera = scene->CreateCamera(); + ASSERT_NE(nullptr, camera); + camera->SetLocalPosition(0.5, 0.5, 0.5); + camera->SetLocalRotation(0, 1.57, 0); + camera->SetImageWidth(32); + camera->SetImageHeight(32); + root->AddChild(camera); + + Image image = camera->CreateImage(); + camera->Capture(image); + + unsigned int height = camera->ImageHeight(); + unsigned int width = camera->ImageWidth(); + unsigned int channelCount = PixelUtil::ChannelCount(camera->ImageFormat()); + unsigned int step = width * channelCount; + unsigned char *data = image.Data(); + for (unsigned int i = 0; i < height; ++i) + { + for (unsigned int j = 0; j < step; j += channelCount) + { + unsigned int idx = i * step + j; + unsigned int r = data[idx]; + unsigned int g = data[idx + 1]; + unsigned int b = data[idx + 2]; + + // color should be a shade of red (submesh0) or green (submesh1) + EXPECT_TRUE(r > 0u || g > 0u); + EXPECT_EQ(b, 0u); + } + } + + // Clean up + engine->DestroyScene(scene); +}