diff --git a/graphics/include/ignition/common/ColladaExporter.hh b/graphics/include/ignition/common/ColladaExporter.hh index 93d4d41d1..5add1a5cb 100644 --- a/graphics/include/ignition/common/ColladaExporter.hh +++ b/graphics/include/ignition/common/ColladaExporter.hh @@ -24,6 +24,7 @@ #include #include +#include #include #include @@ -32,6 +33,41 @@ namespace ignition { namespace common { + /// \brief This struct contains light data specifically for collada export + /// Defaults set based on collada 1.4 specifications + struct ColladaLight + { + /// \brief Name of the light + std::string name; + + /// \brief Type of the light. Either "point", "directional" or "spot" + std::string type; + + /// \brief Light direction (directional/spot lights only) + math::Vector3d direction; + + /// \brief Light position (non directional lights only) + math::Vector3d position; + + /// \brief Light diffuse color + math::Color diffuse; + + /// \brief Constant attentuation + double constantAttenuation = 1.0; + + /// \brief Linear attentuation + double linearAttenuation = 0.0; + + /// \brief Quadratic attentuation + double quadraticAttenuation = 0.0; + + /// \brief Falloff angle in degrees + double falloffAngleDeg = 180.0; + + /// \brief Fallof exponent + double falloffExponent = 0.0; + }; + /// \brief Class used to export Collada mesh files class IGNITION_COMMON_GRAPHICS_VISIBLE ColladaExporter : public MeshExporter { @@ -49,10 +85,28 @@ namespace ignition public: virtual void Export(const Mesh *_mesh, const std::string &_filename, bool _exportTextures = false); + /// \brief Export a mesh to a file + /// \param[in] _mesh Pointer to the mesh to be exported + /// \param[in] _filename Exported file's path and name + /// \param[in] _exportTextures True to export texture images to + /// '../materials/textures' folder + /// \param[in] _submeshToMatrix Matrices of submeshes public: void Export(const Mesh *_mesh, const std::string &_filename, bool _exportTextures, const std::vector &_submeshToMatrix); + /// \brief Export a mesh to a file + /// \param[in] _mesh Pointer to the mesh to be exported + /// \param[in] _filename Exported file's path and name + /// \param[in] _exportTextures True to export texture images to + /// '../materials/textures' folder + /// \param[in] _submeshToMatrix Matrices of submeshes + /// \param[in] _lights List of lights to export + public: void Export(const Mesh *_mesh, + const std::string &_filename, bool _exportTextures, + const std::vector &_submeshToMatrix, + const std::vector &_lights); + /// \brief Pointer to private data. IGN_UTILS_IMPL_PTR(dataPtr) }; diff --git a/graphics/src/ColladaExporter.cc b/graphics/src/ColladaExporter.cc index 101f41cb4..153540162 100644 --- a/graphics/src/ColladaExporter.cc +++ b/graphics/src/ColladaExporter.cc @@ -122,7 +122,8 @@ class ignition::common::ColladaExporter::Implementation /// scenes XML instance public: void ExportVisualScenes( tinyxml2::XMLElement *_libraryVisualScenesXml, - const std::vector &_submeshToMatrix); + const std::vector &_submeshToMatrix, + const std::vector &_lights); /// \brief Export scene element /// \param[in] _sceneXml Pointer to the scene XML instance @@ -164,12 +165,24 @@ void ColladaExporter::Export(const Mesh *_mesh, const std::string &_filename, bool _exportTextures) { std::vector empty; - this->Export(_mesh, _filename, _exportTextures, empty); + std::vector empty_lights; + this->Export(_mesh, _filename, _exportTextures, empty, empty_lights); +} + +////////////////////////////////////////////////// +void ColladaExporter::Export(const Mesh *_mesh, + const std::string &_filename, bool _exportTextures, + const std::vector &_submeshToMatrix) +{ + std::vector empty_lights; + this->Export(_mesh, _filename, _exportTextures, + _submeshToMatrix, empty_lights); } ////////////////////////////////////////////////// void ColladaExporter::Export(const Mesh *_mesh, const std::string &_filename, - bool _exportTextures, const std::vector &_submeshToMatrix) + bool _exportTextures, const std::vector &_submeshToMatrix, + const std::vector &_lights) { if ( _submeshToMatrix.size() > 0 && (_mesh->SubMeshCount() != _submeshToMatrix.size()) ) @@ -239,10 +252,75 @@ void ColladaExporter::Export(const Mesh *_mesh, const std::string &_filename, colladaXml->LinkEndChild(libraryEffectsXml); } + tinyxml2::XMLElement *libraryLightsXml = + xmlDoc.NewElement("library_lights"); + for (const auto& light : _lights) + { + tinyxml2::XMLElement *lightXml = + xmlDoc.NewElement("light"); + lightXml->SetAttribute("name", light.name.c_str()); + lightXml->SetAttribute("id", light.name.c_str()); + libraryLightsXml->LinkEndChild(lightXml); + + tinyxml2::XMLElement *techniqueCommonXml = + xmlDoc.NewElement("technique_common"); + lightXml->LinkEndChild(techniqueCommonXml); + + tinyxml2::XMLElement *lightTypeXml = + xmlDoc.NewElement(light.type.c_str()); + techniqueCommonXml->LinkEndChild(lightTypeXml); + + // color + tinyxml2::XMLElement *colorXml = + xmlDoc.NewElement("color"); + char color_str[100] = { 0 }; + snprintf(color_str, sizeof(color_str), "%g %g %g", + light.diffuse.R(), light.diffuse.G(), light.diffuse.B()); + colorXml->SetText(color_str); + lightTypeXml->LinkEndChild(colorXml); + + // attenuations + if (light.type == "point" || light.type == "spot") + { + auto attenuation_tag = [&](const char* tagname, double value) + { + tinyxml2::XMLElement *attenXml = + xmlDoc.NewElement(tagname); + + char str[100] = { 0 }; + snprintf(str, sizeof(str), "%g", value); + attenXml->SetText(str); + lightTypeXml->LinkEndChild(attenXml); + }; + attenuation_tag("constant_attenuation", light.constantAttenuation); + attenuation_tag("linear_attenuation", light.linearAttenuation); + attenuation_tag("quadratic_attenuation", light.quadraticAttenuation); + } + + // falloff + if (light.type == "spot") + { + tinyxml2::XMLElement *falloffAngleXml = + xmlDoc.NewElement("falloff_angle"); + char str[100] = { 0 }; + snprintf(str, sizeof(str), "%g", light.falloffAngleDeg); + falloffAngleXml->SetText(str); + lightTypeXml->LinkEndChild(falloffAngleXml); + + tinyxml2::XMLElement *falloffExpoXml = + xmlDoc.NewElement("falloff_exponent"); + snprintf(str, sizeof(str), "%g", light.falloffExponent); + falloffExpoXml->SetText(str); + lightTypeXml->LinkEndChild(falloffExpoXml); + } + } + colladaXml->LinkEndChild(libraryLightsXml); + // Library visual scenes element tinyxml2::XMLElement *libraryVisualScenesXml = xmlDoc.NewElement("library_visual_scenes"); - this->dataPtr->ExportVisualScenes(libraryVisualScenesXml, _submeshToMatrix); + this->dataPtr->ExportVisualScenes(libraryVisualScenesXml, + _submeshToMatrix, _lights); colladaXml->LinkEndChild(libraryVisualScenesXml); // Scene element @@ -765,7 +843,8 @@ void ColladaExporter::Implementation::ExportEffects( ////////////////////////////////////////////////// void ColladaExporter::Implementation::ExportVisualScenes( tinyxml2::XMLElement *_libraryVisualScenesXml, - const std::vector &_submeshToMatrix) + const std::vector &_submeshToMatrix, + const std::vector &_lights) { tinyxml2::XMLElement *visualSceneXml = _libraryVisualScenesXml->GetDocument()->NewElement("visual_scene"); @@ -848,6 +927,58 @@ void ColladaExporter::Implementation::ExportVisualScenes( } } } + + for (const auto& light : _lights) + { + tinyxml2::XMLElement *nodeXml = + _libraryVisualScenesXml->GetDocument()->NewElement("node"); + visualSceneXml->LinkEndChild(nodeXml); + + nodeXml->SetAttribute("name", light.name.c_str()); + + tinyxml2::XMLElement *lightInstXml = + _libraryVisualScenesXml->GetDocument()->NewElement("instance_light"); + char lightId[100] = { 0 }; + snprintf(lightId, sizeof(lightId), "#%s", light.name.c_str()); + lightInstXml->SetAttribute("url", lightId); + nodeXml->LinkEndChild(lightInstXml); + + const auto& position = light.position; + // translation + { + tinyxml2::XMLElement *translateXml = + _libraryVisualScenesXml->GetDocument()->NewElement("translate"); + translateXml->SetAttribute("sid", "translate"); + + char tx_value[100] = { 0 }; + snprintf(tx_value, sizeof(tx_value), "%f %f %f", + position.X(), position.Y(), position.Z()); + translateXml->SetText(tx_value); + nodeXml->LinkEndChild(translateXml); + } + + // rotation; blender starts off with light direction (0,0,-1), + // we output an axis-angle rotation to our desired direction + { + auto lightdir_norm = light.direction.Normalized(); + math::Vector3d initial_dir(0, 0, -1); + math::Quaterniond q; + q.From2Axes(initial_dir, lightdir_norm); + + math::Vector3d axis; + double angle = 0.0; + q.ToAxis(axis, angle); + + tinyxml2::XMLElement *rotateXml = + _libraryVisualScenesXml->GetDocument()->NewElement("rotate"); + + char str[100] = { 0 }; + snprintf(str, sizeof(str), "%g %g %g %g", + axis.X(), axis.Y(), axis.Z(), angle / IGN_PI * 180.0); + rotateXml->SetText(str); + nodeXml->LinkEndChild(rotateXml); + } + } } ////////////////////////////////////////////////// diff --git a/graphics/src/ColladaExporter_TEST.cc b/graphics/src/ColladaExporter_TEST.cc index f005b163e..5d1af1b8c 100644 --- a/graphics/src/ColladaExporter_TEST.cc +++ b/graphics/src/ColladaExporter_TEST.cc @@ -315,6 +315,135 @@ TEST_F(ColladaExporter, ExportMeshWithSubmeshes) meshReloaded->TexCoordCount()); } +TEST_F(ColladaExporter, ExportLights) +{ + const auto filenameIn = common::testing::TestFile("data", "box.dae"); + const auto filenameOut = common::joinPaths(this->pathOut, + "box_with_lights_exported"); + const auto filenameOutExt = filenameOut + ".dae"; + + // Load original mesh + common::ColladaLoader loader; + const common::Mesh *meshOriginal = loader.Load(filenameIn); + + // Export with extension + common::ColladaExporter exporter; + std::vector submesh_mtx; + std::vector lights; + + // add some lights + { + common::ColladaLight directional; + directional.name = "sun"; + directional.type = "directional"; + directional.direction = math::Vector3d(0, 1, -1); + directional.position = math::Vector3d(0, 0, 0); + directional.diffuse = math::Color(1, 0.5, 1); + lights.push_back(directional); + + common::ColladaLight point; + point.name = "lamp"; + point.type = "point"; + point.position = math::Vector3d(0, 0, 10); + point.diffuse = math::Color(1, 0.5, 1); + point.constantAttenuation = 0.8; + point.linearAttenuation = 0.8; + point.quadraticAttenuation = 0.1; + lights.push_back(point); + + common::ColladaLight spot; + spot.name = "torch"; + spot.type = "spot"; + spot.position = math::Vector3d(0, 10, 10); + spot.diffuse = math::Color(1, 0.5, 1); + spot.constantAttenuation = 0.8; + spot.linearAttenuation = 0.8; + spot.quadraticAttenuation = 0.1; + spot.falloffAngleDeg = 90.0; + spot.falloffExponent = 0.125; + lights.push_back(spot); + } + + exporter.Export(meshOriginal, filenameOut, false, submesh_mtx, lights); + + tinyxml2::XMLDocument xmlDoc; + ASSERT_EQ(xmlDoc.LoadFile(filenameOutExt.c_str()), tinyxml2::XML_SUCCESS); + + tinyxml2::XMLElement* collada = xmlDoc.FirstChildElement("COLLADA"); + ASSERT_TRUE(xmlDoc.FirstChildElement("COLLADA") != nullptr); + + auto lib_lights = collada->FirstChildElement("library_lights"); + EXPECT_TRUE(lib_lights); + + int light_count = 0; + auto light_ele = lib_lights->FirstChildElement("light"); + for (; light_ele != nullptr; light_ele = light_ele->NextSiblingElement()) + { + const char* light_name_cstr = light_ele->Attribute("name"); + ASSERT_TRUE(light_name_cstr); + std::string light_name = light_name_cstr; + + auto technique = light_ele->FirstChildElement("technique_common"); + EXPECT_TRUE(technique); + + if (light_name == "sun") + { + auto directional = technique->FirstChildElement("directional"); + EXPECT_TRUE(directional); + EXPECT_TRUE(directional->FirstChildElement("color")); + } + else if (light_name == "lamp") + { + auto point = technique->FirstChildElement("point"); + EXPECT_TRUE(point); + EXPECT_TRUE(point->FirstChildElement("color")); + EXPECT_TRUE(point->FirstChildElement("constant_attenuation")); + EXPECT_TRUE(point->FirstChildElement("linear_attenuation")); + EXPECT_TRUE(point->FirstChildElement("quadratic_attenuation")); + } + else if (light_name == "torch") + { + auto spot = technique->FirstChildElement("spot"); + EXPECT_TRUE(spot); + EXPECT_TRUE(spot->FirstChildElement("color")); + EXPECT_TRUE(spot->FirstChildElement("constant_attenuation")); + EXPECT_TRUE(spot->FirstChildElement("linear_attenuation")); + EXPECT_TRUE(spot->FirstChildElement("quadratic_attenuation")); + EXPECT_TRUE(spot->FirstChildElement("falloff_angle")); + EXPECT_TRUE(spot->FirstChildElement("falloff_exponent")); + } + else + ASSERT_TRUE(0); // Invalid light name given + + ++light_count; + } + EXPECT_EQ(light_count, 3); + + // instantiation + auto lib_visual_scenes = collada->FirstChildElement("library_visual_scenes"); + EXPECT_TRUE(lib_visual_scenes); + auto scene = lib_visual_scenes->FirstChildElement("visual_scene"); + EXPECT_TRUE(scene); + + int node_with_light_count = 0; + auto node_ele = scene->FirstChildElement("node"); + for (; node_ele != nullptr; node_ele = node_ele->NextSiblingElement()) + { + const char* node_name_cstr = node_ele->Attribute("name"); + ASSERT_TRUE(node_name_cstr); + std::string node_name = node_name_cstr; + if (node_name == "sun" || node_name == "lamp" || node_name == "torch") + { + EXPECT_TRUE(node_ele->FirstChildElement("instance_light")); + EXPECT_TRUE(node_ele->FirstChildElement("translate")); + EXPECT_TRUE(node_ele->FirstChildElement("rotate")); + + ++node_with_light_count; + } + } + EXPECT_EQ(node_with_light_count, 3); +} + ///////////////////////////////////////////////// int main(int argc, char **argv) {