// -*-c++-*- /* * 3DS reader/writer for Open Scene Graph * * Copyright (C) ??? * * Writing support added 2009 by Sukender (Benoit Neil), http://sukender.free.fr, * strongly inspired by the OBJ writer object by Stephan Huber * * The Open Scene Graph (OSG) is a cross platform C++/OpenGL library for * real-time rendering of large 3D photo-realistic models. * The OSG homepage is http://www.openscenegraph.org/ */ /// [EXPERIMENTAL] Disables animation data (and matrix transforms) for compatibility with some 3rd party apps. /// Animations are not read by all 3DS importers. Thus disabling them may allow some 3rd-party apps, such as Rhinoceros (tested with 4.0) to correctly import 3DS files. /// However, having proper hierarchy with matrix transforms will become impossible. ///\warning This is still experimental, hence the compile flag. This should become a reader/writer option as soon as it works as intented (maybe "noMatrixTransforms" could become a read/write option?). #define DISABLE_3DS_ANIMATION 0 // Default = 0 #include #include #include #include #include "WriterNodeVisitor.h" #include #include void copyOsgMatrixToLib3dsMatrix(Lib3dsMatrix lib3ds_matrix, const osg::Matrix& osg_matrix) { for(int row=0; row<4; ++row) { lib3ds_matrix[row][0] = osg_matrix.ptr()[row*4+0]; lib3ds_matrix[row][1] = osg_matrix.ptr()[row*4+1]; lib3ds_matrix[row][2] = osg_matrix.ptr()[row*4+2]; lib3ds_matrix[row][3] = osg_matrix.ptr()[row*4+3]; } } inline void copyOsgVectorToLib3dsVector(Lib3dsVector lib3ds_vector, const osg::Vec3f& osg_vector) { lib3ds_vector[0] = osg_vector[0]; lib3ds_vector[1] = osg_vector[1]; lib3ds_vector[2] = osg_vector[2]; } inline void copyOsgVectorToLib3dsVector(Lib3dsVector lib3ds_vector, const osg::Vec3d& osg_vector) { lib3ds_vector[0] = osg_vector[0]; lib3ds_vector[1] = osg_vector[1]; lib3ds_vector[2] = osg_vector[2]; } inline void copyOsgColorToLib3dsColor(Lib3dsVector lib3ds_vector, const osg::Vec4f& osg_vector) { lib3ds_vector[0] = osg_vector[0]; lib3ds_vector[1] = osg_vector[1]; lib3ds_vector[2] = osg_vector[2]; } inline void copyOsgColorToLib3dsColor(Lib3dsVector lib3ds_vector, const osg::Vec4d& osg_vector) { lib3ds_vector[0] = osg_vector[0]; lib3ds_vector[1] = osg_vector[1]; lib3ds_vector[2] = osg_vector[2]; } inline void copyOsgQuatToLib3dsQuat(float lib3ds_vector[4], const osg::Quat& osg_quat) { //lib3ds_vector[0] = osg_quat[3]; // Not sure //lib3ds_vector[1] = osg_quat[0]; //lib3ds_vector[2] = osg_quat[1]; //lib3ds_vector[3] = osg_quat[2]; // 3DS seems to store (angle in radians, axis_x, axis_y, axis_z), but it works with (axis_x, axis_y, axis_z, -angle in radians)! osg::Quat::value_type angle, x, y, z; osg_quat.getRotate(angle, x, y, z); lib3ds_vector[0] = static_cast(x); lib3ds_vector[1] = static_cast(y); lib3ds_vector[2] = static_cast(z); lib3ds_vector[3] = static_cast(-angle); } /// Checks if a filename (\b not path) is 8.3 (an empty name is never 8.3, and a path is never 8.3). /// Please note the '8' and '3' limitations are in \b bytes, not in characters (which is different when using UTF8). bool is83(const std::string & s) { // 012345678901 // ABCDEFGH.ABC if (s.find_first_of("/\\") != std::string::npos) return false; // It should not be a path, but a filename unsigned int len = s.length(); if (len > 12 || len == 0) return false; size_t pointPos = s.rfind('.'); if (pointPos == std::string::npos) return len <= 8; // Without point // With point if (pointPos > 8) return false; if (len-1 - pointPos > 3) return false; return true; } inline std::string::size_type maxNameLen(bool extendedFilePaths, bool isNodeName) { if (extendedFilePaths) return 63; return isNodeName ? 8 : (8+1+3); } /// Tests if the given string is a path supported by 3DS format (8.3, 63 chars max, non empty). inline bool is3DSName(const std::string & s, bool extendedFilePaths, bool isNodeName) { unsigned int len = s.length(); if (len > maxNameLen(extendedFilePaths, isNodeName) || len == 0) return false; // Extended paths are simply those that fits the 64 bytes buffer. if (extendedFilePaths) return true; // Short paths simply must have no subdirectory. return is83(s); } // Use namespace qualification to avoid static-link symbol collitions // from multiply defined symbols. namespace plugin3ds { /** writes all primitives of a primitive-set out to a stream, decomposes quads to triangles, line-strips to lines etc */ class PrimitiveIndexWriter : public osg::PrimitiveIndexFunctor { public: PrimitiveIndexWriter(osg::Geometry * geo, ListTriangle & listTriangles, unsigned int drawable_n, unsigned int material) : osg::PrimitiveIndexFunctor(), _drawable_n(drawable_n), _listTriangles(listTriangles), _hasNormalCoords(geo->getNormalArray() != NULL), _hasTexCoords(geo->getTexCoordArray(0) != NULL), _lastFaceIndex(0), _material(material) { } unsigned int getNextFaceIndex() { return _lastFaceIndex; } virtual void setVertexArray(unsigned int,const osg::Vec2*) {} virtual void setVertexArray(unsigned int count,const osg::Vec3* vecs) {} virtual void setVertexArray(unsigned int,const osg::Vec4* ) {} virtual void setVertexArray(unsigned int,const osg::Vec2d*) {} virtual void setVertexArray(unsigned int ,const osg::Vec3d* ) {} virtual void setVertexArray(unsigned int,const osg::Vec4d* ) {} // operator for triangles void writeTriangle(unsigned int i1, unsigned int i2, unsigned int i3) { Triangle triangle; triangle.t1 = i1; triangle.t2 = i2; triangle.t3 = i3; triangle.material = _material; _listTriangles.push_back(ListTriangle::value_type(triangle, _drawable_n)); } virtual void begin(GLenum mode) { _modeCache = mode; _indexCache.clear(); } virtual void vertex(unsigned int vert) { _indexCache.push_back(vert); } virtual void end() { if (!_indexCache.empty()) { drawElements(_modeCache,_indexCache.size(),&_indexCache.front()); } } virtual void drawArrays(GLenum mode,GLint first,GLsizei count); virtual void drawElements(GLenum mode,GLsizei count,const GLubyte* indices) { drawElementsImplementation(mode, count, indices); } virtual void drawElements(GLenum mode,GLsizei count,const GLushort* indices) { drawElementsImplementation(mode, count, indices); } virtual void drawElements(GLenum mode,GLsizei count,const GLuint* indices) { drawElementsImplementation(mode, count, indices); } protected: templatevoid drawElementsImplementation(GLenum mode, GLsizei count, const T* indices) { if (indices==0 || count==0) return; typedef const T* IndexPointer; switch(mode) { case(GL_TRIANGLES): { //lib3ds_mesh_resize_faces(_mesh, _lastFaceIndex + count / 3); IndexPointer ilast = &indices[count]; for(IndexPointer iptr=indices;iptr _indexCache; bool _hasNormalCoords, _hasTexCoords; unsigned int _lastFaceIndex; unsigned int _material; }; void PrimitiveIndexWriter::drawArrays(GLenum mode,GLint first,GLsizei count) { switch(mode) { case(GL_TRIANGLES): { unsigned int pos=first; for(GLsizei i=2;igetDiffuse(osg::Material::FRONT); ambient = mat->getAmbient(osg::Material::FRONT); specular = mat->getSpecular(osg::Material::FRONT); shininess = mat->getShininess(osg::Material::FRONT) / 128.f; // OpenGL shininess = pow(2, 10.0*mat->shininess); (As in lib3ds example) // => mat->shininess = log.2( OpenGL shininess ) /10 (if values are >0) // => mat->shininess = log( OpenGL shininess ) / log(2) /10 //shininess = mat->getShininess(osg::Material::FRONT) <= 0 ? 0 : log( mat->getShininess(osg::Material::FRONT) ) / log(2.f) / 10.f; transparency = 1-diffuse.w(); if (preserveName == false) { name = writerNodeVisitor.getUniqueName(mat->getName(),true,"mat"); } else { name = writerNodeVisitor.getMaterialName(mat->getName()); } osg::StateAttribute * attribute = stateset->getAttribute(osg::StateAttribute::CULLFACE); if (!attribute) { double_sided = true; } else { assert(dynamic_cast(attribute)); osg::CullFace::Mode mode = static_cast(attribute)->getMode(); if (mode == osg::CullFace::BACK) double_sided = false; else if (mode == osg::CullFace::FRONT) { OSG_WARN << "3DS Writer: Reversed face (culled FRONT) not supported yet." << std::endl; double_sided = false; } else { assert(mode == osg::CullFace::FRONT_AND_BACK); OSG_WARN << "3DS Writer: Invisible face (culled FRONT_AND_BACK) not supported yet." << std::endl; double_sided = false; } } } if (tex) { osg::Image* img = tex->getImage(0); if(img) { texture_transparency = (stateset->getMode(GL_BLEND) == osg::StateAttribute::ON); osg::Texture::WrapMode wrapS = tex->getWrap(osg::Texture2D::WRAP_S); texture_no_tile = !(wrapS == osg::Texture2D::REPEAT || wrapS == osg::Texture2D::MIRROR); image = img; } } if (name.empty()) { std::stringstream ss; ss << "m" << index; name = ss.str(); } } /// Converts an extension to a 3-letters long one equivalent. std::string convertExt(const std::string & path, bool extendedFilePaths) { if (extendedFilePaths) return path; // Extensions are not truncated for extended filenames std::string ext = osgDB::getFileExtensionIncludingDot(path); if (ext == ".tiff") ext = ".tif"; else if (ext == ".jpeg") ext = ".jpg"; else if (ext == ".jpeg2000" || ext == ".jpg2000") ext = ".jpc"; return osgDB::getNameLessExtension(path) + ext; } WriterNodeVisitor::WriterNodeVisitor(Lib3dsFile * file3ds, const std::string & fileName, const osgDB::ReaderWriter::Options* options, const std::string & srcDirectory) : osg::NodeVisitor(osg::NodeVisitor::TRAVERSE_ALL_CHILDREN), _succeeded(true), _srcDirectory(srcDirectory), _file3ds(file3ds), _currentStateSet(new osg::StateSet()), _lastMaterialIndex(0), _lastMeshIndex(0), _cur3dsNode(NULL), _options(options), _imageCount(0), _extendedFilePaths(false), _preserveMaterialNames(false) { if (!fileName.empty()) _directory = options->getDatabasePathList().empty() ? osgDB::getFilePath(fileName) : options->getDatabasePathList().front(); if (options) { std::istringstream iss(options->getOptionString()); std::string opt; while (iss >> opt) { if (opt == "extended3dsFilePaths" || opt == "extended3DSFilePaths") _extendedFilePaths = true; if (opt == "preserveMaterialNames") _preserveMaterialNames = true; } } } void WriterNodeVisitor::writeMaterials() { unsigned int nbMat = _materialMap.size(); lib3ds_file_reserve_materials(_file3ds, nbMat, 1); // Ugly thing: it seems lib3ds_file_insert_material() doesn't support insertion in a random order (else materials are not assigned the right way) for (unsigned int iMat=0; iMatsecond; if (mat.index != static_cast(iMat)) continue; // Ugly thing (2) Lib3dsMaterial * mat3ds = lib3ds_material_new(osgDB::getSimpleFileName(mat.name).c_str()); copyOsgColorToLib3dsColor(mat3ds->ambient, mat.ambient); copyOsgColorToLib3dsColor(mat3ds->diffuse, mat.diffuse); copyOsgColorToLib3dsColor(mat3ds->specular, mat.specular); mat3ds->shininess = mat.shininess; mat3ds->transparency = mat.transparency; mat3ds->two_sided = mat.double_sided ? 1 : 0; if (mat.image) { std::string path; ImageSet::const_iterator itImage( _imageSet.find(mat.image.get()) ); if (itImage != _imageSet.end()) { // Image has been already used path = itImage->second; } else { // First time we 'see' this image if (mat.image->getFileName().empty()) { std::ostringstream oss; oss << "Image_" << _imageCount++ << ".rgb"; path = oss.str(); } else { path = osgDB::getPathRelative(_srcDirectory, mat.image->getFileName()); } path = convertExt(path, _extendedFilePaths); path = getUniqueName(path, false, ""); // Write const std::string fullPath( osgDB::concatPaths(_directory, path) ); osgDB::makeDirectoryForFile(fullPath); osgDB::writeImageFile(*(mat.image), fullPath, _options); // Insert in map _imageSet.insert(ImageSet::value_type(mat.image.get(), path)); } Lib3dsTextureMap & tex = mat3ds->texture1_map; strcpy(tex.name, path.c_str()); // Here we don't assume anything about initial flags state (actually it is set to LIB3DS_TEXTURE_NO_TILE by lib3DS, but this is subject to change) if (mat.texture_transparency) tex.flags |= LIB3DS_TEXTURE_ALPHA_SOURCE; else tex.flags &= ~LIB3DS_TEXTURE_ALPHA_SOURCE; if (mat.texture_no_tile) tex.flags |= LIB3DS_TEXTURE_NO_TILE; else tex.flags &= ~LIB3DS_TEXTURE_NO_TILE; } if (!succeeded()) return; lib3ds_file_insert_material(_file3ds, mat3ds, itr->second.index); break; // Ugly thing (3) } } } /// Truncates an UTF8 string so that it does not takes more than a given \b bytes amount (\b excluding the potential NULL end character). /// The function assumes the UTF8 string is valid. ///\return A valid UTF8 string which size is less or equal to \c byteLimit. // May be moved in osgDB/ConvertUTF? std::string utf8TruncateBytes(const std::string & s, std::string::size_type byteLimit) { // Untruncated strings if (s.size() <= byteLimit) return s; // Truncated strings std::string::const_iterator it=s.begin(), itEnd=s.begin()+byteLimit; std::string::const_iterator itStop=it; // Note: itEnd is < s.end(), so that we can always write "it+1" for(; it!=itEnd; ++it) { unsigned char c = static_cast(*it); if ((c & 0x80) == 0) itStop=it+1; // 7 bits ANSI. itStop must then point after that character. else if ((c & 0x40) != 0) itStop=it; // UTF8 sequence start: this is also past-the-end for the previous character (ANSI or UTF8) } return std::string(s.begin(), itStop); } #ifdef OSG_USE_UTF8_FILENAME # define truncateFilenameBytes(str, size) utf8TruncateBytes(str, size) #else # define truncateFilenameBytes(str, size) std::string(str, 0, size) #endif std::string WriterNodeVisitor::getUniqueName(const std::string& _defaultValue, bool isNodeName, const std::string & _defaultPrefix, int currentPrefixLen) { //const unsigned int MAX_LENGTH = maxNameLen(_extendedFilePaths); const unsigned int MAX_PREFIX_LENGTH = _extendedFilePaths ? 52 : 6; // Arbitrarily defined for short names, kept enough room for displaying UINT_MAX (10 characters) for long names. assert(_defaultPrefix.length()<=4); // Default prefix is too long (implementation error) const std::string defaultPrefix(_defaultPrefix.empty() ? "_" : _defaultPrefix); if (currentPrefixLen<0) currentPrefixLen = osg::maximum(_defaultPrefix.length(), _defaultValue.length()); currentPrefixLen = osg::clampBelow(currentPrefixLen, static_cast(MAX_PREFIX_LENGTH)); // Tests if default name is valid and unique NameMap & nameMap = isNodeName ? _nodeNameMap : _imageNameMap; PrefixMap & prefixMap = isNodeName ? _nodePrefixMap : _imagePrefixMap; // Handling of paths is simple. Algorithm: // - For short names, subdirectories are simply forbidden. Use the simple file name. // - Else, the whole (relative) path must simply be <64 chars. // After this, begin enumeration. std::string parentPath, filename, ext, namePrefix; unsigned int max_val = 0; // TODO Move the two parts of this giant if/else into two separate functions for better readability. if (_extendedFilePaths) { // Tests if default name is valid and unique if (is3DSName(_defaultValue, _extendedFilePaths, isNodeName)) { std::pair insertion( nameMap.insert(_defaultValue) ); if (insertion.second) return _defaultValue; // Return if element is newly inserted in the map (else there is a naming collision) } // Simply separate name and last extension filename = osgDB::getNameLessExtension(osgDB::getSimpleFileName(_defaultValue)); if (!isNodeName) { ext = osgDB::getFileExtensionIncludingDot(_defaultValue); if (ext == ".") ext = ""; } // Compute parent path // Pre-condition: paths are supposed to be relative. // If full path is too long (>MAX_PREFIX_LENGTH), cut path to let enough space for simple file name. // Do not cut in the middle of a name, but at path separators. parentPath = osgDB::getFilePath(_defaultValue); if (_defaultValue.length() >MAX_PREFIX_LENGTH) // Not parentPath but _defaultValue! { // Nodes names: keep last directories (used only for the root name, generally named after the full file path) // Images names: keep first directories (for images) if (isNodeName) std::reverse(parentPath.begin(), parentPath.end()); unsigned lenToDelete(filename.length() + ext.length() + 1); lenToDelete = osg::clampBelow(lenToDelete, MAX_PREFIX_LENGTH); parentPath = truncateFilenameBytes(parentPath, MAX_PREFIX_LENGTH - lenToDelete); // +1 for the path separator std::string::size_type separator = parentPath.find_last_of("/\\"); if (separator != std::string::npos) parentPath = parentPath.substr(0, separator); if (isNodeName) std::reverse(parentPath.begin(), parentPath.end()); } // Assert "MAX_PREFIX_LENGTH - parent path length - extension length -1" is >=0 and truncate name to this length to get our new prefix. assert(parentPath.length() + ext.length() <= MAX_PREFIX_LENGTH); const unsigned int len = MAX_PREFIX_LENGTH - (parentPath.length() + ext.length() +1); namePrefix = truncateFilenameBytes(filename, len); if (namePrefix.empty()) namePrefix = defaultPrefix; // Truncate the filename to get our new prefix namePrefix = truncateFilenameBytes(filename, currentPrefixLen); // Enough space has been reserved for UINT_MAX values max_val = UINT_MAX; } else { // Get last extension, and make filename have no extension filename = osgDB::getNameLessAllExtensions(osgDB::getSimpleFileName(_defaultValue)); if (!isNodeName) { ext = truncateFilenameBytes(osgDB::getFileExtensionIncludingDot(_defaultValue), 4); // 4 chars = dot + 3 chars if (ext == ".") ext = ""; } // Tests if STRIPPED default name is valid and unique const std::string strippedName( filename + ext ); if (is3DSName(strippedName, _extendedFilePaths, isNodeName)) { std::pair insertion( nameMap.insert(strippedName) ); if (insertion.second) return strippedName; // Return if element is newly inserted in the map (else there is a naming collision) } namePrefix = filename; if (namePrefix.empty()) namePrefix = defaultPrefix; // Truncate the filename to get our new prefix namePrefix = truncateFilenameBytes(namePrefix, currentPrefixLen); // Compute the maximum enumeration value max_val = static_cast(pow(10., 8. - namePrefix.length())) -1; } assert(namePrefix.size() <= MAX_PREFIX_LENGTH); // Find the current enumeration value (searchStart) unsigned int searchStart(0); PrefixMap::iterator pairPrefix( prefixMap.find(namePrefix) ); if (pairPrefix != prefixMap.end()) { searchStart = pairPrefix->second; } else { // Check if truncated name is ok const std::string res( osgDB::concatPaths(parentPath, namePrefix + ext) ); if (nameMap.find(res) == nameMap.end()) { prefixMap.insert(std::pair(namePrefix, 0)); nameMap.insert(res); return res; } } // Search for a free value for(unsigned int i = searchStart; i <= max_val; ++i) { std::stringstream ss; ss << namePrefix << i; const std::string res( osgDB::concatPaths(parentPath, ss.str() + ext) ); if (nameMap.find(res) == nameMap.end()) { if (pairPrefix != prefixMap.end()) { pairPrefix->second = i + 1; } else { prefixMap.insert(std::pair(namePrefix, i + 1)); } nameMap.insert(res); return res; } } // Failed finding a name // Try with a shorter prefix if possible if (currentPrefixLen>1) return getUniqueName(_defaultValue, isNodeName, defaultPrefix, currentPrefixLen-1); // Try with default prefix if not arleady done if (defaultPrefix != std::string("_")) return getUniqueName(_defaultValue, isNodeName, "_", 1); // No more names OSG_NOTIFY(osg::FATAL) << "No more names available!" << std::endl; _succeeded = false; return "ERROR"; } std::string WriterNodeVisitor::getMaterialName(const std::string& inputMaterialName) { std::string result; // Search in map if this material name already exists MaterialNameMap::const_iterator mapIter=_materialNameMap.find(inputMaterialName); if (mapIter!=_materialNameMap.end()) { // Found, use it ! result = mapIter->second; } else { // Not found, create a new (unique) entry mapped to this material name std::string resultPrefix = inputMaterialName.substr(0, 60); // Up to 64 characters : truncate to 60 characters : 60 + "_" + number result = resultPrefix; NameMap::const_iterator setIter=_materialNameSet.find(result); int suffixValue=-1; while (setIter!=_materialNameSet.end()) { if (suffixValue < 0) { // First add of the suffix resultPrefix = resultPrefix + "_"; suffixValue++; } std::stringstream ss; ss << resultPrefix << suffixValue; result = ss.str(); setIter=_materialNameSet.find(result); suffixValue++; } // Add entry in map _materialNameMap[inputMaterialName] = result; } return result; } int WriterNodeVisitor::processStateSet(osg::StateSet* ss) { MaterialMap::const_iterator itr = _materialMap.find(ss); if (itr != _materialMap.end()) { assert(itr->second.index>=0); return itr->second.index; } osg::Material* mat = dynamic_cast(ss->getAttribute(osg::StateAttribute::MATERIAL)); osg::Texture* tex = dynamic_cast(ss->getTextureAttribute(0, osg::StateAttribute::TEXTURE)); if (mat || tex) { int matNum = _lastMaterialIndex; _materialMap.insert(std::make_pair(osg::ref_ptr(ss), Material(*this, ss, mat, tex, _preserveMaterialNames, matNum) )); ++_lastMaterialIndex; return matNum; } return -1; } /** * Add a vertice to the index and link it with the Triangle index and the drawable. * \param index_vert is the map where the vertice are stored. * \param index is the indice of the vertice's position in the vec3. * \param drawable_n is the number of the drawable. * \return the position of the vertice in the final mesh. */ unsigned int WriterNodeVisitor::getMeshIndexForGeometryIndex(MapIndices & index_vert, unsigned int index, unsigned int drawable_n) { MapIndices::iterator itIndex = index_vert.find(std::pair(index, drawable_n)); if (itIndex == index_vert.end()) { unsigned int indexMesh = index_vert.size(); index_vert.insert(std::make_pair(std::pair(index, drawable_n), indexMesh)); return indexMesh; } return itIndex->second; } void WriterNodeVisitor::buildMesh(osg::Geode & geo, const osg::Matrix & mat, MapIndices & index_vert, bool texcoords, Lib3dsMesh * mesh) { OSG_DEBUG << "Building Mesh" << std::endl; assert(mesh); // Write points assert(index_vert.size() <= MAX_VERTICES); lib3ds_mesh_resize_vertices(mesh, index_vert.size(), texcoords ? 1 : 0, 0); for(MapIndices::iterator it = index_vert.begin(); it != index_vert.end();++it) { osg::Geometry *g = geo.getDrawable( it->first.second )->asGeometry(); const osg::Array * basevecs = g->getVertexArray(); assert(basevecs); if (!basevecs || basevecs->getNumElements()==0) continue; if (basevecs->getType() == osg::Array::Vec3ArrayType) { const osg::Vec3Array & vecs= *static_cast(basevecs); copyOsgVectorToLib3dsVector(mesh->vertices[it->second], vecs[it->first.first]*mat); } else if (basevecs->getType() == osg::Array::Vec3dArrayType) { // Handle double presision vertices by converting them to float with a warning OSG_NOTICE << "3DS format only supports single precision vertices. Converting double precision to single." << std::endl; const osg::Vec3dArray & vecs= *static_cast(basevecs); copyOsgVectorToLib3dsVector(mesh->vertices[it->second], vecs[it->first.first]*mat); } else { OSG_NOTIFY(osg::FATAL) << "Vertex array is not Vec3 or Vec3d. Not implemented" << std::endl; _succeeded = false; return; } } // Write texture coords (Texture 0 only) if (texcoords) { for(MapIndices::iterator it = index_vert.begin(); it != index_vert.end(); ++it) { osg::Geometry *g = geo.getDrawable( it->first.second )->asGeometry(); const osg::Array * texarray = g->getNumTexCoordArrays()>=1 ? g->getTexCoordArray(0) : NULL; if (!texarray || texarray->getNumElements()==0) continue; if (g->getTexCoordArray(0)->getType() != osg::Array::Vec2ArrayType) { OSG_NOTIFY(osg::FATAL) << "Texture coords array is not Vec2. Not implemented" << std::endl; _succeeded = false; return; } const osg::Vec2Array & vecs= *static_cast(texarray); mesh->texcos[it->second][0] = vecs[it->first.first][0]; mesh->texcos[it->second][1] = vecs[it->first.first][1]; } } lib3ds_file_insert_mesh(_file3ds, mesh, _lastMeshIndex); ++_lastMeshIndex; Lib3dsMeshInstanceNode * node3ds = lib3ds_node_new_mesh_instance(mesh, mesh->name, NULL, NULL, NULL); lib3ds_file_append_node(_file3ds, reinterpret_cast(node3ds), reinterpret_cast(_cur3dsNode)); } unsigned int WriterNodeVisitor::calcVertices(osg::Geode & geo) { unsigned int numVertice = 0; for (unsigned int i = 0; i < geo.getNumDrawables(); ++i) { osg::Geometry *g = geo.getDrawable( i )->asGeometry(); if (g!=NULL && g->getVertexArray()) numVertice += g->getVertexArray()->getNumElements(); } return numVertice; } void WriterNodeVisitor::buildFaces(osg::Geode & geo, const osg::Matrix & mat, ListTriangle & listTriangles, bool texcoords) { unsigned int nbTrianglesRemaining = listTriangles.size(); unsigned int nbVerticesRemaining = calcVertices(geo); // May set _succeded to false if (!succeeded()) return; std::string name( getUniqueName(geo.getName().empty() ? geo.className() : geo.getName(), true, "geo") ); if (!succeeded()) return; Lib3dsMesh *mesh = lib3ds_mesh_new( name.c_str() ); if (!mesh) { OSG_NOTIFY(osg::FATAL) << "Allocation error" << std::endl; _succeeded = false; return; } //copyOsgMatrixToLib3dsMatrix(mesh->matrix, mat); lib3ds_mesh_resize_faces (mesh, osg::minimum(nbTrianglesRemaining, MAX_FACES)); lib3ds_mesh_resize_vertices(mesh, osg::minimum(nbVerticesRemaining, MAX_VERTICES), texcoords ? 0 : 1, 0); // Not mandatory but will allocate once a big block // Test if the mesh will be split and needs sorting if (nbVerticesRemaining >= MAX_VERTICES || nbTrianglesRemaining >= MAX_FACES) { OSG_INFO << "Sorting elements..." << std::endl; WriterCompareTriangle cmp(geo, nbVerticesRemaining); std::sort(listTriangles.begin(), listTriangles.end(), cmp); } MapIndices index_vert; unsigned int numFace = 0; // Current face index for (ListTriangle::iterator it = listTriangles.begin(); it != listTriangles.end(); ++it) //Go through the triangle list to define meshs { // Test if the mesh will be full after adding a face if (index_vert.size()+3 >= MAX_VERTICES || numFace+1 >= MAX_FACES) { // Finnish mesh lib3ds_mesh_resize_faces (mesh, numFace); //lib3ds_mesh_resize_vertices() will be called in buildMesh() buildMesh(geo, mat, index_vert, texcoords, mesh); // May set _succeded to false if (!succeeded()) { lib3ds_mesh_free(mesh); return; } // "Reset" values and start over a new mesh index_vert.clear(); nbTrianglesRemaining -= numFace; numFace = 0; // We can't call a thing like "nbVerticesRemaining -= ...;" because points may be used multiple times. // [Sukender: An optimisation here would take too much time I think.] mesh = lib3ds_mesh_new( getUniqueName(geo.getName().empty() ? geo.className() : geo.getName(), true, "geo").c_str()); if (!mesh) { OSG_NOTIFY(osg::FATAL) << "Allocation error" << std::endl; _succeeded = false; return; } lib3ds_mesh_resize_faces (mesh, osg::minimum(nbTrianglesRemaining, MAX_FACES)); lib3ds_mesh_resize_vertices(mesh, osg::minimum(nbVerticesRemaining, MAX_VERTICES), texcoords ? 0 : 1, 0); // Not mandatory but will allocate once a big block } Lib3dsFace & face = mesh->faces[numFace++]; face.index[0] = getMeshIndexForGeometryIndex(index_vert, it->first.t1, it->second); face.index[1] = getMeshIndexForGeometryIndex(index_vert, it->first.t2, it->second); face.index[2] = getMeshIndexForGeometryIndex(index_vert, it->first.t3, it->second); face.material = it->first.material; } buildMesh(geo, mat, index_vert, texcoords, mesh); // May set _succeded to false if (!succeeded()) { lib3ds_mesh_free(mesh); return; } } void WriterNodeVisitor::createListTriangle(osg::Geometry * geo, ListTriangle & listTriangles, bool & texcoords, unsigned int & drawable_n) { const osg::Array * basevecs = geo->getVertexArray(); if (!basevecs || basevecs->getNumElements()==0) return; // Texture coords const osg::Array * basetexvecs = geo->getNumTexCoordArrays()>=1 ? geo->getTexCoordArray(0) : NULL; if (basetexvecs) { unsigned int nb = basetexvecs->getNumElements(); if (nb != geo->getVertexArray()->getNumElements()) { OSG_NOTIFY(osg::FATAL) << "There are more/less texture coords than vertices (corrupted geometry)" << std::endl; _succeeded = false; return; } texcoords = true; } int material = processStateSet(_currentStateSet.get()); for(unsigned int i = 0; i < geo->getNumPrimitiveSets(); ++i) //Fill the Triangle List { osg::PrimitiveSet* ps = geo->getPrimitiveSet(i); PrimitiveIndexWriter pif(geo, listTriangles, drawable_n, material); ps->accept(pif); } } void WriterNodeVisitor::apply( osg::Geode &node ) { pushStateSet(node.getStateSet()); //_nameStack.push_back(node.getName()); unsigned int count = node.getNumDrawables(); ListTriangle listTriangles; bool texcoords = false; for ( unsigned int i = 0; i < count; i++ ) { osg::Geometry *g = node.getDrawable( i )->asGeometry(); if ( g != NULL ) { pushStateSet(g->getStateSet()); createListTriangle(g, listTriangles, texcoords, i); // May set _succeded to false popStateSet(g->getStateSet()); if (!succeeded()) break; } } if (succeeded() && count > 0) { #if DISABLE_3DS_ANIMATION osg::Matrix mat( osg::computeLocalToWorld(getNodePath()) ); buildFaces(node, mat, listTriangles, texcoords); // May set _succeded to false #else buildFaces(node, osg::Matrix(), listTriangles, texcoords); // May set _succeded to false #endif } popStateSet(node.getStateSet()); //_nameStack.pop_back(); if (succeeded()) traverse(node); } void WriterNodeVisitor::apply( osg::Billboard &node ) { // TODO Does not handle Billboards' points yet pushStateSet(node.getStateSet()); Lib3dsMeshInstanceNode * parent = _cur3dsNode; unsigned int count = node.getNumDrawables(); ListTriangle listTriangles; bool texcoords = false; OSG_NOTICE << "Warning: 3DS writer is incomplete for Billboards (rotation not implemented)." << std::endl; #if DISABLE_3DS_ANIMATION osg::Matrix m( osg::computeLocalToWorld(getNodePath()) ); #endif for ( unsigned int i = 0; i < count; i++ ) { osg::Geometry *g = node.getDrawable( i )->asGeometry(); if ( g != NULL ) { listTriangles.clear(); _cur3dsNode = parent; pushStateSet(g->getStateSet()); createListTriangle(g, listTriangles, texcoords, i); popStateSet(g->getStateSet()); // May set _succeded to false if (!succeeded()) break; osg::Matrix pointLocalMat(osg::Matrix::translate(node.getPosition(i))); // TODO handle rotation #if DISABLE_3DS_ANIMATION osg::Matrix currentBillboardWorldMat(pointLocalMat * m); apply3DSMatrixNode(node, &pointLocalMat, "bil"); // Add a 3DS matrix node (with local matrix) buildFaces(node, currentBillboardWorldMat, listTriangles, texcoords); // May set _succeded to false #else apply3DSMatrixNode(node, &pointLocalMat, "bil"); // Add a 3DS matrix node (with local matrix) buildFaces(node, osg::Matrix(), listTriangles, texcoords); // May set _succeded to false #endif if (!succeeded()) break; } } if (succeeded()) traverse(node); _cur3dsNode = parent; popStateSet(node.getStateSet()); } void WriterNodeVisitor::apply(osg::Group &node) { pushStateSet(node.getStateSet()); Lib3dsMeshInstanceNode * parent = _cur3dsNode; #if DISABLE_3DS_ANIMATION osg::Matrix mat( osg::computeLocalToWorld(getNodePath()) ); apply3DSMatrixNode(node, &mat, "grp"); #else apply3DSMatrixNode(node, NULL, "grp"); #endif if (succeeded()) traverse(node); _cur3dsNode = parent; popStateSet(node.getStateSet()); } void WriterNodeVisitor::apply(osg::MatrixTransform &node) { pushStateSet(node.getStateSet()); Lib3dsMeshInstanceNode * parent = _cur3dsNode; #if DISABLE_3DS_ANIMATION osg::Matrix mat( osg::computeLocalToWorld(getNodePath()) ); #else osg::Matrix mat( node.getMatrix() ); #endif apply3DSMatrixNode(node, &mat, "mtx"); if (succeeded()) traverse(node); _cur3dsNode = parent; popStateSet(node.getStateSet()); } void WriterNodeVisitor::apply3DSMatrixNode(osg::Node &node, const osg::Matrix * m, const char * const prefix) { // Note: Creating a mesh instance with no transform and then copying the matrix doesn't work (matrix seems to be a temporary/computed value) Lib3dsMeshInstanceNode * parent = _cur3dsNode; Lib3dsMeshInstanceNode * node3ds = NULL; if (m) { osg::Vec3 osgScl, osgPos; osg::Quat osgRot, osgSo; m->decompose(osgPos, osgRot, osgScl, osgSo); float pos[3]; float scl[3]; float rot[4]; copyOsgVectorToLib3dsVector(pos, osgPos); copyOsgVectorToLib3dsVector(scl, osgScl); copyOsgQuatToLib3dsQuat(rot, osgRot); node3ds = lib3ds_node_new_mesh_instance(NULL, getUniqueName(node.getName().empty() ? node.className() : node.getName(), true, prefix).c_str(), pos, scl, rot); } else { node3ds = lib3ds_node_new_mesh_instance(NULL, getUniqueName(node.getName().empty() ? node.className() : node.getName(), true, prefix).c_str(), NULL, NULL, NULL); } lib3ds_file_append_node(_file3ds, reinterpret_cast(node3ds), reinterpret_cast(parent)); _cur3dsNode = node3ds; } // end namespace plugin3ds }