mesh_loader.cpp
Go to the documentation of this file.
00001 /*
00002  * Copyright (c) 2010, Willow Garage, Inc.
00003  * All rights reserved.
00004  *
00005  * Redistribution and use in source and binary forms, with or without
00006  * modification, are permitted provided that the following conditions are met:
00007  *
00008  *     * Redistributions of source code must retain the above copyright
00009  *       notice, this list of conditions and the following disclaimer.
00010  *     * Redistributions in binary form must reproduce the above copyright
00011  *       notice, this list of conditions and the following disclaimer in the
00012  *       documentation and/or other materials provided with the distribution.
00013  *     * Neither the name of the Willow Garage, Inc. nor the names of its
00014  *       contributors may be used to endorse or promote products derived from
00015  *       this software without specific prior written permission.
00016  *
00017  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
00018  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
00019  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
00020  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
00021  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
00022  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
00023  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
00024  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
00025  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
00026  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
00027  * POSSIBILITY OF SUCH DAMAGE.
00028  */
00029 
00030 #include "mesh_loader.h"
00031 #include <resource_retriever/retriever.h>
00032 
00033 #include <boost/filesystem.hpp>
00034 
00035 #include <ogre_tools/stl_loader.h>
00036 
00037 #include <OGRE/OgreMeshManager.h>
00038 #include <OGRE/OgreTextureManager.h>
00039 #include <OGRE/OgreMaterialManager.h>
00040 #include <OGRE/OgreTexture.h>
00041 #include <OGRE/OgrePass.h>
00042 #include <OGRE/OgreTechnique.h>
00043 #include <OGRE/OgreMaterial.h>
00044 #include <OGRE/OgreTextureUnitState.h>
00045 #include <OGRE/OgreMeshSerializer.h>
00046 #include <OGRE/OgreSubMesh.h>
00047 #include <OGRE/OgreHardwareBufferManager.h>
00048 
00049 #include <ros/assert.h>
00050 
00051 #include <assimp/assimp.hpp>
00052 #include <assimp/aiScene.h>
00053 #include <assimp/aiPostProcess.h>
00054 #include <assimp/IOStream.h>
00055 #include <assimp/IOSystem.h>
00056 
00057 
00058 namespace fs = boost::filesystem;
00059 
00060 namespace rviz
00061 {
00062 
00063 class ResourceIOStream : public Assimp::IOStream
00064 {
00065 public:
00066   ResourceIOStream(const resource_retriever::MemoryResource& res)
00067   : res_(res)
00068   , pos_(res.data.get())
00069   {}
00070 
00071   ~ResourceIOStream()
00072   {}
00073 
00074   size_t Read(void* buffer, size_t size, size_t count)
00075   {
00076     size_t to_read = size * count;
00077     if (pos_ + to_read > res_.data.get() + res_.size)
00078     {
00079       to_read = res_.size - (pos_ - res_.data.get());
00080     }
00081 
00082     memcpy(buffer, pos_, to_read);
00083     pos_ += to_read;
00084 
00085     return to_read;
00086   }
00087 
00088   size_t Write( const void* buffer, size_t size, size_t count) { ROS_BREAK(); return 0; }
00089 
00090   aiReturn Seek( size_t offset, aiOrigin origin)
00091   {
00092     uint8_t* new_pos = 0;
00093     switch (origin)
00094     {
00095     case aiOrigin_SET:
00096       new_pos = res_.data.get() + offset;
00097       break;
00098     case aiOrigin_CUR:
00099       new_pos = pos_ + offset; // TODO is this right?  can offset really not be negative
00100       break;
00101     case aiOrigin_END:
00102       new_pos = res_.data.get() + res_.size - offset; // TODO is this right?
00103       break;
00104     default:
00105       ROS_BREAK();
00106     }
00107 
00108     if (new_pos < res_.data.get() || new_pos > res_.data.get() + res_.size)
00109     {
00110       return aiReturn_FAILURE;
00111     }
00112 
00113     pos_ = new_pos;
00114     return aiReturn_SUCCESS;
00115   }
00116 
00117   size_t Tell() const
00118   {
00119     return pos_ - res_.data.get();
00120   }
00121 
00122   size_t FileSize() const
00123   {
00124     return res_.size;
00125   }
00126 
00127   void Flush() {}
00128 
00129 private:
00130   resource_retriever::MemoryResource res_;
00131   uint8_t* pos_;
00132 };
00133 
00134 class ResourceIOSystem : public Assimp::IOSystem
00135 {
00136 public:
00137   ResourceIOSystem()
00138   {
00139   }
00140 
00141   ~ResourceIOSystem()
00142   {
00143   }
00144 
00145   // Check whether a specific file exists
00146   bool Exists(const char* file) const
00147   {
00148     // Ugly -- two retrievals where there should be one (Exists + Open)
00149     // resource_retriever needs a way of checking for existence
00150     // TODO: cache this
00151     resource_retriever::MemoryResource res;
00152     try
00153     {
00154       res = retriever_.get(file);
00155     }
00156     catch (resource_retriever::Exception& e)
00157     {
00158       return false;
00159     }
00160 
00161     return true;
00162   }
00163 
00164   // Get the path delimiter character we'd like to see
00165   char getOsSeparator() const
00166   {
00167     return '/';
00168   }
00169 
00170   // ... and finally a method to open a custom stream
00171   Assimp::IOStream* Open(const char* file, const char* mode)
00172   {
00173     ROS_ASSERT(mode == std::string("r") || mode == std::string("rb"));
00174 
00175     // Ugly -- two retrievals where there should be one (Exists + Open)
00176     // resource_retriever needs a way of checking for existence
00177     resource_retriever::MemoryResource res;
00178     try
00179     {
00180       res = retriever_.get(file);
00181     }
00182     catch (resource_retriever::Exception& e)
00183     {
00184       return 0;
00185     }
00186 
00187     return new ResourceIOStream(res);
00188   }
00189 
00190   void Close(Assimp::IOStream* stream) { delete stream; }
00191 
00192 private:
00193   mutable resource_retriever::Retriever retriever_;
00194 };
00195 
00196 // Mostly stolen from gazebo
00197 void buildMesh(const aiScene* scene, const aiNode* node, const Ogre::MeshPtr& mesh, Ogre::AxisAlignedBox& aabb, float& radius)
00198 {
00199   if (!node)
00200   {
00201     return;
00202   }
00203 
00204   aiMatrix4x4 transform = node->mTransformation;
00205   aiNode *pnode = node->mParent;
00206   while (pnode)
00207   {
00208     // Don't convert to y-up orientation, which is what the root node in
00209     // Assimp does
00210     if (pnode->mParent != NULL)
00211       transform = pnode->mTransformation * transform;
00212     pnode = pnode->mParent;
00213   }
00214 
00215   aiMatrix3x3 rotation(transform);
00216   aiMatrix3x3 inverse_transpose_rotation(rotation);
00217   inverse_transpose_rotation.Inverse();
00218   inverse_transpose_rotation.Transpose();
00219 
00220   for (uint32_t i = 0; i < node->mNumMeshes; i++)
00221   {
00222     aiMesh* input_mesh = scene->mMeshes[node->mMeshes[i]];
00223 
00224     Ogre::SubMesh* submesh = mesh->createSubMesh();
00225     submesh->useSharedVertices = false;
00226     submesh->vertexData = new Ogre::VertexData();
00227     Ogre::VertexData* vertex_data = submesh->vertexData;
00228     Ogre::VertexDeclaration* vertex_decl = vertex_data->vertexDeclaration;
00229 
00230     size_t offset = 0;
00231     // positions
00232     vertex_decl->addElement(0, offset, Ogre::VET_FLOAT3, Ogre::VES_POSITION);
00233     offset += Ogre::VertexElement::getTypeSize(Ogre::VET_FLOAT3);
00234 
00235     // normals
00236     if (input_mesh->HasNormals())
00237     {
00238       vertex_decl->addElement(0, offset, Ogre::VET_FLOAT3, Ogre::VES_NORMAL);
00239       offset += Ogre::VertexElement::getTypeSize(Ogre::VET_FLOAT3);
00240     }
00241 
00242     // texture coordinates (only support 1 for now)
00243     if (input_mesh->HasTextureCoords(0))
00244     {
00245       vertex_decl->addElement(0, offset, Ogre::VET_FLOAT2, Ogre::VES_TEXTURE_COORDINATES, 0);
00246       offset += Ogre::VertexElement::getTypeSize(Ogre::VET_FLOAT2);
00247     }
00248 
00249     // todo vertex colors
00250 
00251     // allocate the vertex buffer
00252     vertex_data->vertexCount = input_mesh->mNumVertices;
00253     Ogre::HardwareVertexBufferSharedPtr vbuf = Ogre::HardwareBufferManager::getSingleton().createVertexBuffer(vertex_decl->getVertexSize(0),
00254                                                                           vertex_data->vertexCount,
00255                                                                           Ogre::HardwareBuffer::HBU_STATIC_WRITE_ONLY,
00256                                                                           false);
00257 
00258     vertex_data->vertexBufferBinding->setBinding(0, vbuf);
00259     float* vertices = static_cast<float*>(vbuf->lock(Ogre::HardwareBuffer::HBL_DISCARD));
00260 
00261     // Add the vertices
00262     for (uint32_t j = 0; j < input_mesh->mNumVertices; j++)
00263     {
00264       aiVector3D p = input_mesh->mVertices[j];
00265       p *= transform;
00266       *vertices++ = p.x;
00267       *vertices++ = p.y;
00268       *vertices++ = p.z;
00269 
00270       Ogre::Vector3 v(p.x, p.y, p.z);
00271       aabb.merge(v);
00272       float dist = v.length();
00273       if (dist > radius)
00274       {
00275         radius = dist;
00276       }
00277 
00278       if (input_mesh->HasNormals())
00279       {
00280               aiVector3D n = inverse_transpose_rotation * input_mesh->mNormals[j];
00281         *vertices++ = n.x;
00282         *vertices++ = n.y;
00283         *vertices++ = n.z;
00284       }
00285 
00286       if (input_mesh->HasTextureCoords(0))
00287       {
00288         *vertices++ = input_mesh->mTextureCoords[0][j].x;
00289         *vertices++ = input_mesh->mTextureCoords[0][j].y;
00290       }
00291     }
00292 
00293     // calculate index count
00294     submesh->indexData->indexCount = 0;
00295     for (uint32_t j = 0; j < input_mesh->mNumFaces; j++)
00296     {
00297       aiFace& face = input_mesh->mFaces[j];
00298       submesh->indexData->indexCount += face.mNumIndices;
00299     }
00300 
00301     // If we have less than 65536 (2^16) vertices, we can use a 16-bit index buffer.
00302     if( vertex_data->vertexCount < (1<<16) )
00303     {
00304       // allocate index buffer
00305       submesh->indexData->indexBuffer = Ogre::HardwareBufferManager::getSingleton().createIndexBuffer(
00306         Ogre::HardwareIndexBuffer::IT_16BIT,
00307         submesh->indexData->indexCount,
00308         Ogre::HardwareBuffer::HBU_STATIC_WRITE_ONLY,
00309         false);
00310 
00311       Ogre::HardwareIndexBufferSharedPtr ibuf = submesh->indexData->indexBuffer;
00312       uint16_t* indices = static_cast<uint16_t*>(ibuf->lock(Ogre::HardwareBuffer::HBL_DISCARD));
00313 
00314       // add the indices
00315       for (uint32_t j = 0; j < input_mesh->mNumFaces; j++)
00316       {
00317         aiFace& face = input_mesh->mFaces[j];
00318         for (uint32_t k = 0; k < face.mNumIndices; ++k)
00319         {
00320           *indices++ = face.mIndices[k];
00321         }
00322       }
00323 
00324       ibuf->unlock();
00325     }
00326     else 
00327     {
00328       // Else we have more than 65536 (2^16) vertices, so we must
00329       // use a 32-bit index buffer (or subdivide the mesh, which
00330       // I'm too impatient to do right now)
00331 
00332       // allocate index buffer
00333       submesh->indexData->indexBuffer = Ogre::HardwareBufferManager::getSingleton().createIndexBuffer(
00334         Ogre::HardwareIndexBuffer::IT_32BIT,
00335         submesh->indexData->indexCount,
00336         Ogre::HardwareBuffer::HBU_STATIC_WRITE_ONLY,
00337         false);
00338 
00339       Ogre::HardwareIndexBufferSharedPtr ibuf = submesh->indexData->indexBuffer;
00340       uint32_t* indices = static_cast<uint32_t*>(ibuf->lock(Ogre::HardwareBuffer::HBL_DISCARD));
00341 
00342       // add the indices
00343       for (uint32_t j = 0; j < input_mesh->mNumFaces; j++)
00344       {
00345         aiFace& face = input_mesh->mFaces[j];
00346         for (uint32_t k = 0; k < face.mNumIndices; ++k)
00347         {
00348           *indices++ = face.mIndices[k];
00349         }
00350       }
00351 
00352       ibuf->unlock();
00353     }
00354     vbuf->unlock();
00355   }
00356 
00357   for (uint32_t i=0; i < node->mNumChildren; ++i)
00358   {
00359     buildMesh(scene, node->mChildren[i], mesh, aabb, radius);
00360   }
00361 }
00362 
00363 void loadTexture(const std::string& resource_path)
00364 {
00365   if (!Ogre::TextureManager::getSingleton().resourceExists(resource_path))
00366   {
00367     resource_retriever::Retriever retriever;
00368     resource_retriever::MemoryResource res;
00369     try
00370     {
00371       res = retriever.get(resource_path);
00372     }
00373     catch (resource_retriever::Exception& e)
00374     {
00375       ROS_ERROR("%s", e.what());
00376     }
00377 
00378     if (res.size != 0)
00379     {
00380       Ogre::DataStreamPtr stream(new Ogre::MemoryDataStream(res.data.get(), res.size));
00381       Ogre::Image image;
00382       std::string extension = fs::extension(fs::path(resource_path));
00383 
00384       if (extension[0] == '.')
00385       {
00386         extension = extension.substr(1, extension.size() - 1);
00387       }
00388 
00389       try
00390       {
00391         image.load(stream, extension);
00392         Ogre::TextureManager::getSingleton().loadImage(resource_path, Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, image);
00393       }
00394       catch (Ogre::Exception& e)
00395       {
00396         ROS_ERROR("Could not load texture [%s]: %s", resource_path.c_str(), e.what());
00397       }
00398     }
00399   }
00400 }
00401 
00402 // Mostly cribbed from gazebo
00403 void loadMaterialsForMesh(const std::string& resource_path, const aiScene* scene, const Ogre::MeshPtr& mesh)
00404 {
00405   std::vector<Ogre::MaterialPtr> material_lookup;
00406 
00407   for (uint32_t i = 0; i < scene->mNumMaterials; i++)
00408   {
00409     std::stringstream ss;
00410     ss << resource_path << "Material" << i;
00411     Ogre::MaterialPtr mat = Ogre::MaterialManager::getSingleton().create(ss.str(), ROS_PACKAGE_NAME, true);
00412     material_lookup.push_back(mat);
00413 
00414     Ogre::Technique* tech = mat->getTechnique(0);
00415     Ogre::Pass* pass = tech->getPass(0);
00416 
00417     aiMaterial *amat = scene->mMaterials[i];
00418 
00419     Ogre::ColourValue diffuse(1.0, 1.0, 1.0, 1.0);
00420     Ogre::ColourValue specular(1.0, 1.0, 1.0, 1.0);
00421     Ogre::ColourValue ambient(0.5, 0.5, 0.5, 1.0);
00422 
00423     for (uint32_t j=0; j < amat->mNumProperties; j++)
00424     {
00425       aiMaterialProperty *prop = amat->mProperties[j];
00426       std::string propKey = prop->mKey.data;
00427 
00428       if (propKey == "$tex.file")
00429       {
00430         aiString texName;
00431         aiTextureMapping mapping;
00432         uint32_t uvIndex;
00433         amat->GetTexture(aiTextureType_DIFFUSE,0, &texName, &mapping, &uvIndex);
00434 
00435         // Assume textures are in paths relative to the mesh
00436         std::string texture_path = fs::path(resource_path).parent_path().string() + "/" + texName.data;
00437         loadTexture(texture_path);
00438         Ogre::TextureUnitState* tu = pass->createTextureUnitState();
00439         tu->setTextureName(texture_path);
00440       }
00441       else if (propKey == "$clr.diffuse")
00442       {
00443         aiColor3D clr;
00444         amat->Get(AI_MATKEY_COLOR_DIFFUSE, clr);
00445         diffuse = Ogre::ColourValue(clr.r, clr.g, clr.b);
00446       }
00447       else if (propKey == "$clr.ambient")
00448       {
00449         aiColor3D clr;
00450         amat->Get(AI_MATKEY_COLOR_AMBIENT, clr);
00451 
00452         // Most of are DAE files don't have ambient color defined
00453         if (clr.r > 0 && clr.g > 0 && clr.b > 0)
00454         {
00455           ambient = Ogre::ColourValue(clr.r, clr.g, clr.b);
00456         }
00457       }
00458       else if (propKey == "$clr.specular")
00459       {
00460         aiColor3D clr;
00461         amat->Get(AI_MATKEY_COLOR_SPECULAR, clr);
00462         specular = Ogre::ColourValue(clr.r, clr.g, clr.b);
00463       }
00464       else if (propKey == "$clr.emissive")
00465       {
00466         aiColor3D clr;
00467         amat->Get(AI_MATKEY_COLOR_EMISSIVE, clr);
00468         mat->setSelfIllumination(clr.r, clr.g, clr.b);
00469       }
00470       else if (propKey == "$clr.opacity")
00471       {
00472         float o;
00473         amat->Get(AI_MATKEY_OPACITY, o);
00474         diffuse.a = o;
00475       }
00476       else if (propKey == "$mat.shininess")
00477       {
00478         float s;
00479         amat->Get(AI_MATKEY_SHININESS, s);
00480         mat->setShininess(s);
00481       }
00482       else if (propKey == "$mat.shadingm")
00483       {
00484         int model;
00485         amat->Get(AI_MATKEY_SHADING_MODEL, model);
00486         switch(model)
00487         {
00488           case aiShadingMode_Flat:
00489             mat->setShadingMode(Ogre::SO_FLAT);
00490             break;
00491           case aiShadingMode_Phong:
00492             mat->setShadingMode(Ogre::SO_PHONG);
00493             break;
00494           case aiShadingMode_Gouraud:
00495           default:
00496             mat->setShadingMode(Ogre::SO_GOURAUD);
00497             break;
00498         }
00499       }
00500     }
00501 
00502     int mode = aiBlendMode_Default;
00503     amat->Get(AI_MATKEY_BLEND_FUNC, mode);
00504     switch(mode)
00505     {
00506       case aiBlendMode_Additive:
00507         mat->setSceneBlending(Ogre::SBT_ADD);
00508         break;
00509       case aiBlendMode_Default:
00510       default:
00511         {
00512           if (diffuse.a < 0.99)
00513           {
00514             pass->setSceneBlending(Ogre::SBT_TRANSPARENT_ALPHA);
00515           }
00516           else
00517           {
00518             pass->setSceneBlending(Ogre::SBT_REPLACE);
00519           }
00520         }
00521         break;
00522     }
00523 
00524     mat->setAmbient(ambient);
00525     mat->setDiffuse(diffuse);
00526     specular.a = diffuse.a;
00527     mat->setSpecular(specular);
00528   }
00529 
00530   for (uint32_t i = 0; i < mesh->getNumSubMeshes(); ++i)
00531   {
00532     mesh->getSubMesh(i)->setMaterialName(material_lookup[scene->mMeshes[i]->mMaterialIndex]->getName());
00533   }
00534 }
00535 
00536 Ogre::MeshPtr meshFromAssimpScene(const std::string& name, const aiScene* scene)
00537 {
00538   if (!scene->HasMeshes())
00539   {
00540     ROS_ERROR("No meshes found in file [%s]", name.c_str());
00541     return Ogre::MeshPtr();
00542   }
00543 
00544   Ogre::MeshPtr mesh = Ogre::MeshManager::getSingleton().createManual(name, ROS_PACKAGE_NAME);
00545 
00546   Ogre::AxisAlignedBox aabb(Ogre::AxisAlignedBox::EXTENT_NULL);
00547   float radius = 0.0f;
00548   buildMesh(scene, scene->mRootNode, mesh, aabb, radius);
00549 
00550   mesh->_setBounds(aabb);
00551   mesh->_setBoundingSphereRadius(radius);
00552   mesh->buildEdgeList();
00553 
00554   loadMaterialsForMesh(name, scene, mesh);
00555 
00556   mesh->load();
00557 
00558   return mesh;
00559 }
00560 
00561 Ogre::MeshPtr loadMeshFromResource(const std::string& resource_path)
00562 {
00563   if (Ogre::MeshManager::getSingleton().resourceExists(resource_path))
00564   {
00565     return Ogre::MeshManager::getSingleton().getByName(resource_path);
00566   }
00567   else
00568   {
00569     fs::path model_path(resource_path);
00570 #if BOOST_FILESYSTEM_VERSION == 3
00571     std::string ext = model_path.extension().string();
00572 #else
00573     std::string ext = model_path.extension();
00574 #endif
00575     if (ext == ".mesh" || ext == ".MESH")
00576     {
00577       resource_retriever::Retriever retriever;
00578       resource_retriever::MemoryResource res;
00579       try
00580       {
00581         res = retriever.get(resource_path);
00582       }
00583       catch (resource_retriever::Exception& e)
00584       {
00585         ROS_ERROR("%s", e.what());
00586         return Ogre::MeshPtr();
00587       }
00588 
00589       if (res.size == 0)
00590       {
00591         return Ogre::MeshPtr();
00592       }
00593 
00594       Ogre::MeshSerializer ser;
00595       Ogre::DataStreamPtr stream(new Ogre::MemoryDataStream(res.data.get(), res.size));
00596       Ogre::MeshPtr mesh = Ogre::MeshManager::getSingleton().createManual(resource_path, "rviz");
00597       ser.importMesh(stream, mesh.get());
00598 
00599       return mesh;
00600     }
00601     else if (ext == ".stl" || ext == ".STL" || ext == ".stlb" || ext == ".STLB")
00602     {
00603       resource_retriever::Retriever retriever;
00604       resource_retriever::MemoryResource res;
00605       try
00606       {
00607         res = retriever.get(resource_path);
00608       }
00609       catch (resource_retriever::Exception& e)
00610       {
00611         ROS_ERROR("%s", e.what());
00612         return Ogre::MeshPtr();
00613       }
00614 
00615       if (res.size == 0)
00616       {
00617         return Ogre::MeshPtr();
00618       }
00619 
00620       ogre_tools::STLLoader loader;
00621       if (!loader.load(res.data.get()))
00622       {
00623         ROS_ERROR("Failed to load file [%s]", resource_path.c_str());
00624         return Ogre::MeshPtr();
00625       }
00626 
00627       return loader.toMesh(resource_path);
00628     }
00629     else
00630     {
00631       Assimp::Importer importer;
00632       importer.SetIOHandler(new ResourceIOSystem());
00633       const aiScene* scene = importer.ReadFile(resource_path, aiProcess_SortByPType|aiProcess_GenNormals|aiProcess_Triangulate|aiProcess_GenUVCoords|aiProcess_FlipUVs);
00634       if (!scene)
00635       {
00636         ROS_ERROR("Could not load resource [%s]: %s", resource_path.c_str(), importer.GetErrorString());
00637         return Ogre::MeshPtr();
00638       }
00639 
00640       return meshFromAssimpScene(resource_path, scene);
00641     }
00642   }
00643 
00644   return Ogre::MeshPtr();
00645 }
00646 
00647 }


rviz
Author(s): Dave Hershberger, Josh Faust
autogenerated on Mon Jan 6 2014 11:54:32