From 2c7f607e7c47461d60e8d371ce2f2cece1559a18 Mon Sep 17 00:00:00 2001 From: RichardTea <31507749+RichardTea@users.noreply.github.com> Date: Thu, 11 Jul 2019 12:22:43 +0100 Subject: [PATCH 01/19] Create ZipArchiveIOSystem Moved IOSystem Unzip functionality out of D3MF Importer into include/assimp Cleaned up and more efficient: Don't map the archive until the caller opens a file or requests the contents list Don't extract any files until they are opened Store the location of the data within the ZIP and extract it later if the file is opened. --- code/3MF/D3MFImporter.cpp | 8 +- code/3MF/D3MFOpcPackage.cpp | 345 +----------------- code/3MF/D3MFOpcPackage.h | 7 +- code/Common/ZipArchiveIOSystem.cpp | 521 ++++++++++++++++++++++++++++ include/assimp/ZipArchiveIOSystem.h | 40 +++ 5 files changed, 568 insertions(+), 353 deletions(-) create mode 100644 code/Common/ZipArchiveIOSystem.cpp create mode 100644 include/assimp/ZipArchiveIOSystem.h diff --git a/code/3MF/D3MFImporter.cpp b/code/3MF/D3MFImporter.cpp index 2898f8ac2..682de684a 100644 --- a/code/3MF/D3MFImporter.cpp +++ b/code/3MF/D3MFImporter.cpp @@ -50,6 +50,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include +#include #include #include @@ -58,11 +59,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include "D3MFOpcPackage.h" -#ifdef ASSIMP_USE_HUNTER -# include -#else -# include -#endif #include #include "3MFXmlTags.h" #include @@ -453,7 +449,7 @@ bool D3MFImporter::CanRead(const std::string &filename, IOSystem *pIOHandler, bo if ( nullptr == pIOHandler ) { return false; } - if ( !D3MF::D3MFOpcPackage::isZipArchive( pIOHandler, filename ) ) { + if ( !ZipArchiveIOSystem::isZipArchive( pIOHandler, filename ) ) { return false; } D3MF::D3MFOpcPackage opcPackage( pIOHandler, filename ); diff --git a/code/3MF/D3MFOpcPackage.cpp b/code/3MF/D3MFOpcPackage.cpp index 3a6e4dbc9..739e0cfcc 100644 --- a/code/3MF/D3MFOpcPackage.cpp +++ b/code/3MF/D3MFOpcPackage.cpp @@ -49,6 +49,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include +#include #include #include @@ -56,344 +57,11 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include -#ifdef ASSIMP_USE_HUNTER -# include -#else -# include -#endif #include "3MFXmlTags.h" namespace Assimp { namespace D3MF { - -class IOSystem2Unzip { -public: - static voidpf open(voidpf opaque, const char* filename, int mode); - static uLong read(voidpf opaque, voidpf stream, void* buf, uLong size); - static uLong write(voidpf opaque, voidpf stream, const void* buf, uLong size); - static long tell(voidpf opaque, voidpf stream); - static long seek(voidpf opaque, voidpf stream, uLong offset, int origin); - static int close(voidpf opaque, voidpf stream); - static int testerror(voidpf opaque, voidpf stream); - static zlib_filefunc_def get(IOSystem* pIOHandler); -}; - -voidpf IOSystem2Unzip::open(voidpf opaque, const char* filename, int mode) { - IOSystem* io_system = reinterpret_cast(opaque); - - const char* mode_fopen = NULL; - if((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER)==ZLIB_FILEFUNC_MODE_READ) { - mode_fopen = "rb"; - } else { - if(mode & ZLIB_FILEFUNC_MODE_EXISTING) { - mode_fopen = "r+b"; - } else { - if(mode & ZLIB_FILEFUNC_MODE_CREATE) { - mode_fopen = "wb"; - } - } - } - - return (voidpf) io_system->Open(filename, mode_fopen); -} - -uLong IOSystem2Unzip::read(voidpf /*opaque*/, voidpf stream, void* buf, uLong size) { - IOStream* io_stream = (IOStream*) stream; - - return static_cast(io_stream->Read(buf, 1, size)); -} - -uLong IOSystem2Unzip::write(voidpf /*opaque*/, voidpf stream, const void* buf, uLong size) { - IOStream* io_stream = (IOStream*) stream; - - return static_cast(io_stream->Write(buf, 1, size)); -} - -long IOSystem2Unzip::tell(voidpf /*opaque*/, voidpf stream) { - IOStream* io_stream = (IOStream*) stream; - - return static_cast(io_stream->Tell()); -} - -long IOSystem2Unzip::seek(voidpf /*opaque*/, voidpf stream, uLong offset, int origin) { - IOStream* io_stream = (IOStream*) stream; - - aiOrigin assimp_origin; - switch (origin) { - default: - case ZLIB_FILEFUNC_SEEK_CUR: - assimp_origin = aiOrigin_CUR; - break; - case ZLIB_FILEFUNC_SEEK_END: - assimp_origin = aiOrigin_END; - break; - case ZLIB_FILEFUNC_SEEK_SET: - assimp_origin = aiOrigin_SET; - break; - } - - return (io_stream->Seek(offset, assimp_origin) == aiReturn_SUCCESS ? 0 : -1); -} - -int IOSystem2Unzip::close(voidpf opaque, voidpf stream) { - IOSystem* io_system = (IOSystem*) opaque; - IOStream* io_stream = (IOStream*) stream; - - io_system->Close(io_stream); - - return 0; -} - -int IOSystem2Unzip::testerror(voidpf /*opaque*/, voidpf /*stream*/) { - return 0; -} - -zlib_filefunc_def IOSystem2Unzip::get(IOSystem* pIOHandler) { - zlib_filefunc_def mapping; - -#ifdef ASSIMP_USE_HUNTER - mapping.zopen_file = (open_file_func)open; - mapping.zread_file = (read_file_func)read; - mapping.zwrite_file = (write_file_func)write; - mapping.ztell_file = (tell_file_func)tell; - mapping.zseek_file = (seek_file_func)seek; - mapping.zclose_file = (close_file_func)close; - mapping.zerror_file = (error_file_func)testerror; -#else - mapping.zopen_file = open; - mapping.zread_file = read; - mapping.zwrite_file = write; - mapping.ztell_file = tell; - mapping.zseek_file = seek; - mapping.zclose_file = close; - mapping.zerror_file = testerror; -#endif - mapping.opaque = reinterpret_cast(pIOHandler); - - return mapping; -} - -class ZipFile : public IOStream { - friend class D3MFZipArchive; - -public: - explicit ZipFile(size_t size); - virtual ~ZipFile(); - size_t Read(void* pvBuffer, size_t pSize, size_t pCount ); - size_t Write(const void* /*pvBuffer*/, size_t /*pSize*/, size_t /*pCount*/); - size_t FileSize() const; - aiReturn Seek(size_t /*pOffset*/, aiOrigin /*pOrigin*/); - size_t Tell() const; - void Flush(); - -private: - void *m_Buffer; - size_t m_Size; -}; - -ZipFile::ZipFile(size_t size) -: m_Buffer( nullptr ) -, m_Size(size) { - ai_assert(m_Size != 0); - m_Buffer = ::malloc(m_Size); -} - -ZipFile::~ZipFile() { - ::free(m_Buffer); - m_Buffer = NULL; -} - -size_t ZipFile::Read(void* pvBuffer, size_t pSize, size_t pCount) { - const size_t size = pSize * pCount; - ai_assert(size <= m_Size); - - std::memcpy(pvBuffer, m_Buffer, size); - - return size; -} - -size_t ZipFile::Write(const void* pvBuffer, size_t size, size_t pCount ) { - const size_t size_to_write( size * pCount ); - if ( 0 == size_to_write ) { - return 0U; - } - return 0U; -} - -size_t ZipFile::FileSize() const { - return m_Size; -} - -aiReturn ZipFile::Seek(size_t /*pOffset*/, aiOrigin /*pOrigin*/) { - return aiReturn_FAILURE; -} - -size_t ZipFile::Tell() const { - return 0; -} - -void ZipFile::Flush() { - // empty -} - -class D3MFZipArchive : public IOSystem { -public: - static const unsigned int FileNameSize = 256; - - D3MFZipArchive(IOSystem* pIOHandler, const std::string & rFile); - ~D3MFZipArchive(); - bool Exists(const char* pFile) const; - char getOsSeparator() const; - IOStream* Open(const char* pFile, const char* pMode = "rb"); - void Close(IOStream* pFile); - bool isOpen() const; - void getFileList(std::vector &rFileList); - -private: - bool mapArchive(); - -private: - unzFile m_ZipFileHandle; - std::map m_ArchiveMap; -}; - -// ------------------------------------------------------------------------------------------------ -// Constructor. -D3MFZipArchive::D3MFZipArchive(IOSystem* pIOHandler, const std::string& rFile) -: m_ZipFileHandle( nullptr ) -, m_ArchiveMap() { - if (! rFile.empty()) { - zlib_filefunc_def mapping = IOSystem2Unzip::get(pIOHandler); - - m_ZipFileHandle = unzOpen2(rFile.c_str(), &mapping); - if(m_ZipFileHandle != nullptr ) { - mapArchive(); - } - } -} - -// ------------------------------------------------------------------------------------------------ -// Destructor. -D3MFZipArchive::~D3MFZipArchive() { - for(auto &file : m_ArchiveMap) { - delete file.second; - } - m_ArchiveMap.clear(); - - if(m_ZipFileHandle != nullptr) { - unzClose(m_ZipFileHandle); - m_ZipFileHandle = nullptr; - } -} - -// ------------------------------------------------------------------------------------------------ -// Returns true, if the archive is already open. -bool D3MFZipArchive::isOpen() const { - return (m_ZipFileHandle != nullptr ); -} - -// ------------------------------------------------------------------------------------------------ -// Returns true, if the filename is part of the archive. -bool D3MFZipArchive::Exists(const char* pFile) const { - ai_assert(pFile != nullptr ); - - if ( pFile == nullptr ) { - return false; - } - - std::string filename(pFile); - std::map::const_iterator it = m_ArchiveMap.find(filename); - bool exist( false ); - if(it != m_ArchiveMap.end()) { - exist = true; - } - - return exist; -} - -// ------------------------------------------------------------------------------------------------ -// Returns the separator delimiter. -char D3MFZipArchive::getOsSeparator() const { -#ifndef _WIN32 - return '/'; -#else - return '\\'; -#endif -} - -// ------------------------------------------------------------------------------------------------ -// Opens a file, which is part of the archive. -IOStream *D3MFZipArchive::Open(const char* pFile, const char* /*pMode*/) { - ai_assert(pFile != NULL); - - IOStream* result = NULL; - - std::map::iterator it = m_ArchiveMap.find(pFile); - - if(it != m_ArchiveMap.end()) { - result = static_cast(it->second); - } - - return result; -} - -// ------------------------------------------------------------------------------------------------ -// Close a filestream. -void D3MFZipArchive::Close(IOStream *pFile) { - (void)(pFile); - ai_assert(pFile != NULL); - - // We don't do anything in case the file would be opened again in the future -} -// ------------------------------------------------------------------------------------------------ -// Returns the file-list of the archive. -void D3MFZipArchive::getFileList(std::vector &rFileList) { - rFileList.clear(); - - for(const auto &file : m_ArchiveMap) { - rFileList.push_back(file.first); - } -} - -// ------------------------------------------------------------------------------------------------ -// Maps the archive content. -bool D3MFZipArchive::mapArchive() { - bool success = false; - - if(m_ZipFileHandle != NULL) { - if(m_ArchiveMap.empty()) { - // At first ensure file is already open - if(unzGoToFirstFile(m_ZipFileHandle) == UNZ_OK) { - // Loop over all files - do { - char filename[FileNameSize]; - unz_file_info fileInfo; - - if(unzGetCurrentFileInfo(m_ZipFileHandle, &fileInfo, filename, FileNameSize, NULL, 0, NULL, 0) == UNZ_OK) { - // The file has EXACTLY the size of uncompressed_size. In C - // you need to mark the last character with '\0', so add - // another character - if(fileInfo.uncompressed_size != 0 && unzOpenCurrentFile(m_ZipFileHandle) == UNZ_OK) { - std::pair::iterator, bool> result = m_ArchiveMap.insert(std::make_pair(filename, new ZipFile(fileInfo.uncompressed_size))); - - if(unzReadCurrentFile(m_ZipFileHandle, result.first->second->m_Buffer, fileInfo.uncompressed_size) == (long int) fileInfo.uncompressed_size) { - if(unzCloseCurrentFile(m_ZipFileHandle) == UNZ_OK) { - // Nothing to do anymore... - } - } - } - } - } while(unzGoToNextFile(m_ZipFileHandle) != UNZ_END_OF_LIST_OF_FILE); - } - } - - success = true; - } - - return success; -} - // ------------------------------------------------------------------------------------------------ typedef std::shared_ptr OpcPackageRelationshipPtr; @@ -453,7 +121,7 @@ public: D3MFOpcPackage::D3MFOpcPackage(IOSystem* pIOHandler, const std::string& rFile) : mRootStream(nullptr) , mZipArchive() { - mZipArchive.reset( new D3MF::D3MFZipArchive( pIOHandler, rFile ) ); + mZipArchive.reset( new ZipArchiveIOSystem( pIOHandler, rFile ) ); if(!mZipArchive->isOpen()) { throw DeadlyImportError("Failed to open file " + rFile+ "."); } @@ -516,15 +184,6 @@ bool D3MFOpcPackage::validate() { return mZipArchive->Exists( ModelRef.c_str() ); } -bool D3MFOpcPackage::isZipArchive( IOSystem* pIOHandler, const std::string& rFile ) { - D3MF::D3MFZipArchive ar( pIOHandler, rFile ); - if ( !ar.isOpen() ) { - return false; - } - - return true; -} - std::string D3MFOpcPackage::ReadPackageRootRelationship(IOStream* stream) { std::unique_ptr xmlStream(new CIrrXML_IOStreamReader(stream)); std::unique_ptr xml(irr::io::createIrrXMLReader(xmlStream.get())); diff --git a/code/3MF/D3MFOpcPackage.h b/code/3MF/D3MFOpcPackage.h index 47c67f45f..87d172116 100644 --- a/code/3MF/D3MFOpcPackage.h +++ b/code/3MF/D3MFOpcPackage.h @@ -49,6 +49,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include namespace Assimp { + class ZipArchiveIOSystem; + namespace D3MF { using XmlReader = irr::io::IrrXMLReader ; @@ -60,22 +62,19 @@ struct OpcPackageRelationship { std::string target; }; -class D3MFZipArchive; - class D3MFOpcPackage { public: D3MFOpcPackage( IOSystem* pIOHandler, const std::string& rFile ); ~D3MFOpcPackage(); IOStream* RootStream() const; bool validate(); - static bool isZipArchive( IOSystem* pIOHandler, const std::string& rFile ); protected: std::string ReadPackageRootRelationship(IOStream* stream); private: IOStream* mRootStream; - std::unique_ptr mZipArchive; + std::unique_ptr mZipArchive; }; } // Namespace D3MF diff --git a/code/Common/ZipArchiveIOSystem.cpp b/code/Common/ZipArchiveIOSystem.cpp new file mode 100644 index 000000000..5a42a0dcb --- /dev/null +++ b/code/Common/ZipArchiveIOSystem.cpp @@ -0,0 +1,521 @@ +/* +Open Asset Import Library (assimp) +---------------------------------------------------------------------- + +Copyright (c) 2006-2019, assimp team + + +All rights reserved. + +Redistribution and use of this software in source and binary forms, +with or without modification, are permitted provided that the +following conditions are met: + +* Redistributions of source code must retain the above + copyright notice, this list of conditions and the + following disclaimer. + +* Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the + following disclaimer in the documentation and/or other + materials provided with the distribution. + +* Neither the name of the assimp team, nor the names of its + contributors may be used to endorse or promote products + derived from this software without specific prior + written permission of the assimp team. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +---------------------------------------------------------------------- +*/ + +#include +#include + +#include + +#include + +#ifdef ASSIMP_USE_HUNTER +# include +#else +# include +#endif + +namespace Assimp { + + // ---------------------------------------------------------------- + // Wraps an existing Assimp::IOSystem for unzip + class IOSystem2Unzip { + public: + static voidpf open(voidpf opaque, const char* filename, int mode); + static uLong read(voidpf opaque, voidpf stream, void* buf, uLong size); + static uLong write(voidpf opaque, voidpf stream, const void* buf, uLong size); + static long tell(voidpf opaque, voidpf stream); + static long seek(voidpf opaque, voidpf stream, uLong offset, int origin); + static int close(voidpf opaque, voidpf stream); + static int testerror(voidpf opaque, voidpf stream); + static zlib_filefunc_def get(IOSystem* pIOHandler); + }; + + voidpf IOSystem2Unzip::open(voidpf opaque, const char* filename, int mode) { + IOSystem* io_system = reinterpret_cast(opaque); + + const char* mode_fopen = nullptr; + if ((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER) == ZLIB_FILEFUNC_MODE_READ) { + mode_fopen = "rb"; + } + else { + if (mode & ZLIB_FILEFUNC_MODE_EXISTING) { + mode_fopen = "r+b"; + } + else { + if (mode & ZLIB_FILEFUNC_MODE_CREATE) { + mode_fopen = "wb"; + } + } + } + + return (voidpf)io_system->Open(filename, mode_fopen); + } + + uLong IOSystem2Unzip::read(voidpf /*opaque*/, voidpf stream, void* buf, uLong size) { + IOStream* io_stream = (IOStream*)stream; + + return static_cast(io_stream->Read(buf, 1, size)); + } + + uLong IOSystem2Unzip::write(voidpf /*opaque*/, voidpf stream, const void* buf, uLong size) { + IOStream* io_stream = (IOStream*)stream; + + return static_cast(io_stream->Write(buf, 1, size)); + } + + long IOSystem2Unzip::tell(voidpf /*opaque*/, voidpf stream) { + IOStream* io_stream = (IOStream*)stream; + + return static_cast(io_stream->Tell()); + } + + long IOSystem2Unzip::seek(voidpf /*opaque*/, voidpf stream, uLong offset, int origin) { + IOStream* io_stream = (IOStream*)stream; + + aiOrigin assimp_origin; + switch (origin) { + default: + case ZLIB_FILEFUNC_SEEK_CUR: + assimp_origin = aiOrigin_CUR; + break; + case ZLIB_FILEFUNC_SEEK_END: + assimp_origin = aiOrigin_END; + break; + case ZLIB_FILEFUNC_SEEK_SET: + assimp_origin = aiOrigin_SET; + break; + } + + return (io_stream->Seek(offset, assimp_origin) == aiReturn_SUCCESS ? 0 : -1); + } + + int IOSystem2Unzip::close(voidpf opaque, voidpf stream) { + IOSystem* io_system = (IOSystem*)opaque; + IOStream* io_stream = (IOStream*)stream; + + io_system->Close(io_stream); + + return 0; + } + + int IOSystem2Unzip::testerror(voidpf /*opaque*/, voidpf /*stream*/) { + return 0; + } + + zlib_filefunc_def IOSystem2Unzip::get(IOSystem* pIOHandler) { + zlib_filefunc_def mapping; + +#ifdef ASSIMP_USE_HUNTER + mapping.zopen_file = (open_file_func)open; + mapping.zread_file = (read_file_func)read; + mapping.zwrite_file = (write_file_func)write; + mapping.ztell_file = (tell_file_func)tell; + mapping.zseek_file = (seek_file_func)seek; + mapping.zclose_file = (close_file_func)close; + mapping.zerror_file = (error_file_func)testerror; +#else + mapping.zopen_file = open; + mapping.zread_file = read; + mapping.zwrite_file = write; + mapping.ztell_file = tell; + mapping.zseek_file = seek; + mapping.zclose_file = close; + mapping.zerror_file = testerror; +#endif + mapping.opaque = reinterpret_cast(pIOHandler); + + return mapping; + } + + // ---------------------------------------------------------------- + // A read-only file inside a ZIP + + class ZipFile : public IOStream { + friend class ZipFileInfo; + explicit ZipFile(size_t size); + public: + virtual ~ZipFile(); + + // IOStream interface + size_t Read(void* pvBuffer, size_t pSize, size_t pCount) override; + size_t Write(const void* /*pvBuffer*/, size_t /*pSize*/, size_t /*pCount*/) override { return 0; } + size_t FileSize() const override; + aiReturn Seek(size_t pOffset, aiOrigin pOrigin) override; + size_t Tell() const override; + void Flush() override {} + + private: + size_t m_Size = 0; + size_t m_SeekPtr = 0; + std::unique_ptr m_Buffer; + }; + + + // ---------------------------------------------------------------- + // Info about a read-only file inside a ZIP + class ZipFileInfo + { + public: + explicit ZipFileInfo(unzFile zip_handle, size_t size); + + // Allocate and Extract data from the ZIP + ZipFile * Extract(unzFile zip_handle, const std::string & file) const; + + private: + size_t m_Size = 0; + unz_file_pos_s m_ZipFilePos = { 0,0 }; + }; + + ZipFileInfo::ZipFileInfo(unzFile zip_handle, size_t size) + : m_Size(size) { + ai_assert(m_Size != 0); + unzGetFilePos(zip_handle, &(m_ZipFilePos)); + } + + ZipFile * ZipFileInfo::Extract(unzFile zip_handle, const std::string & file) const { + // Find in the ZIP. This cannot fail + unz_file_pos_s *filepos = const_cast(&(m_ZipFilePos)); + if (unzGoToFilePos(zip_handle, filepos) != UNZ_OK) + return nullptr; + + if (unzOpenCurrentFile(zip_handle) != UNZ_OK) + return nullptr; + + ZipFile *zip_file = new ZipFile(m_Size); + + if (unzReadCurrentFile(zip_handle, zip_file->m_Buffer.get(), static_cast(m_Size)) != m_Size) + { + // Failed, release the memory + delete zip_file; + zip_file = nullptr; + } + + ai_assert(unzCloseCurrentFile(zip_handle) == UNZ_OK); + return zip_file; + } + + ZipFile::ZipFile(size_t size) + : m_Size(size) { + ai_assert(m_Size != 0); + m_Buffer = std::unique_ptr(new uint8_t[m_Size]); + } + + ZipFile::~ZipFile() { + } + + size_t ZipFile::Read(void* pvBuffer, size_t pSize, size_t pCount) { + const size_t size = pSize * pCount; + ai_assert((size + m_SeekPtr) <= m_Size); + + std::memcpy(pvBuffer, m_Buffer.get() + m_SeekPtr, size); + + m_SeekPtr += size; + + return size; + } + + size_t ZipFile::FileSize() const { + return m_Size; + } + + aiReturn ZipFile::Seek(size_t pOffset, aiOrigin pOrigin) { + switch (pOrigin) + { + case aiOrigin_SET: { + if (pOffset > m_Size) return aiReturn_FAILURE; + m_SeekPtr = pOffset; + return aiReturn_SUCCESS; + } + + case aiOrigin_CUR: { + if ((pOffset + m_SeekPtr) > m_Size) return aiReturn_FAILURE; + m_SeekPtr += pOffset; + return aiReturn_SUCCESS; + } + + case aiOrigin_END: { + if (pOffset > m_Size) return aiReturn_FAILURE; + m_SeekPtr = m_Size - pOffset; + return aiReturn_SUCCESS; + } + } + + return aiReturn_FAILURE; + } + + size_t ZipFile::Tell() const { + return m_SeekPtr; + } + + // ---------------------------------------------------------------- + // pImpl of the Zip Archive IO + class ZipArchiveIOSystem::Implement { + public: + static const unsigned int FileNameSize = 256; + + Implement(IOSystem* pIOHandler, const char* pFilename, const char* pMode); + ~Implement(); + + bool isOpen() const; + void getFileList(std::vector& rFileList); + void getFileListExtension(std::vector& rFileList, const std::string& extension); + bool Exists(std::string& filename); + IOStream* OpenFile(std::string& filename); + + static void SimplifyFilename(std::string& filename); + + private: + void MapArchive(); + + private: + unzFile m_ZipFileHandle = nullptr; + typedef std::unordered_map ZipFileMap; + ZipFileMap m_ArchiveMap; + }; + + ZipArchiveIOSystem::Implement::Implement(IOSystem* pIOHandler, const char* pFilename, const char* pMode) { + ai_assert(strcmp(pMode, "r") == 0); + ai_assert(pFilename != nullptr); + if (pFilename[0] == 0) + return; + + zlib_filefunc_def mapping = IOSystem2Unzip::get(pIOHandler); + m_ZipFileHandle = unzOpen2(pFilename, &mapping); + } + + ZipArchiveIOSystem::Implement::~Implement() { + m_ArchiveMap.clear(); + + if (m_ZipFileHandle != nullptr) { + unzClose(m_ZipFileHandle); + m_ZipFileHandle = nullptr; + } + } + + void ZipArchiveIOSystem::Implement::MapArchive() { + if (m_ZipFileHandle == nullptr) + return; + + if (!m_ArchiveMap.empty()) + return; + + // At first ensure file is already open + if (unzGoToFirstFile(m_ZipFileHandle) != UNZ_OK) + return; + + // Loop over all files + do { + char filename[FileNameSize]; + unz_file_info fileInfo; + + if (unzGetCurrentFileInfo(m_ZipFileHandle, &fileInfo, filename, FileNameSize, nullptr, 0, nullptr, 0) == UNZ_OK) { + if (fileInfo.uncompressed_size != 0) { + std::string filename_string(filename, fileInfo.size_filename); + SimplifyFilename(filename_string); + std::pair result = m_ArchiveMap.insert(std::make_pair(filename_string, ZipFileInfo(m_ZipFileHandle, fileInfo.uncompressed_size))); + } + } + } while (unzGoToNextFile(m_ZipFileHandle) != UNZ_END_OF_LIST_OF_FILE); + } + + bool ZipArchiveIOSystem::Implement::isOpen() const { + return (m_ZipFileHandle != nullptr); + } + + void ZipArchiveIOSystem::Implement::getFileList(std::vector& rFileList) { + MapArchive(); + rFileList.clear(); + + for (const auto &file : m_ArchiveMap) { + rFileList.push_back(file.first); + } + } + + void ZipArchiveIOSystem::Implement::getFileListExtension(std::vector& rFileList, const std::string& extension) { + MapArchive(); + rFileList.clear(); + + for (const auto &file : m_ArchiveMap) { + if (extension == BaseImporter::GetExtension(file.first)) + rFileList.push_back(file.first); + } + } + + bool ZipArchiveIOSystem::Implement::Exists(std::string& filename) { + MapArchive(); + + ZipFileMap::const_iterator it = m_ArchiveMap.find(filename); + bool exist(false); + + return (it != m_ArchiveMap.end()); + } + + IOStream * ZipArchiveIOSystem::Implement::OpenFile(std::string& filename) { + MapArchive(); + + SimplifyFilename(filename); + + // Find in the map + ZipFileMap::const_iterator zip_it = m_ArchiveMap.find(filename); + if (zip_it == m_ArchiveMap.cend()) + return nullptr; + + const ZipFileInfo &zip_file = (*zip_it).second; + return zip_file.Extract(m_ZipFileHandle, filename); + } + + inline void ReplaceAll(std::string& data, const std::string& before, const std::string& after) { + size_t pos = data.find(before); + while (pos != std::string::npos) + { + data.replace(pos, before.size(), after); + pos = data.find(before, pos + after.size()); + } + } + + inline void ReplaceAllChar(std::string& data, const char before, const char after) { + size_t pos = data.find(before); + while (pos != std::string::npos) + { + data[pos] = after; + pos = data.find(before, pos + 1); + } + } + + void ZipArchiveIOSystem::Implement::SimplifyFilename(std::string& filename) + { + ReplaceAllChar(filename, '\\', '/'); + + // Remove all . and / from the beginning of the path + size_t pos = filename.find_first_not_of("./"); + if (pos != 0) + filename.erase(0, pos); + + // Simplify "my/folder/../file.png" constructions, if any + static const std::string relative("/../"); + const size_t relsize = relative.size() - 1; + pos = filename.find(relative); + while (pos != std::string::npos) + { + // Previous slash + size_t prevpos = filename.rfind('/', pos - 1); + if (prevpos == pos) + filename.erase(0, pos + relative.size()); + else + filename.erase(prevpos, pos + relsize - prevpos); + + pos = filename.find(relative); + } + } + + ZipArchiveIOSystem::ZipArchiveIOSystem(IOSystem* pIOHandler, const char* pFilename, const char* pMode) + : pImpl(new Implement(pIOHandler, pFilename, pMode)) { + } + + // ---------------------------------------------------------------- + // The ZipArchiveIO + ZipArchiveIOSystem::ZipArchiveIOSystem(IOSystem* pIOHandler, const std::string& rFilename, const char* pMode) + : pImpl(new Implement(pIOHandler, rFilename.c_str(), pMode)) + { + } + + ZipArchiveIOSystem::~ZipArchiveIOSystem() { + delete pImpl; + } + + bool ZipArchiveIOSystem::Exists(const char* pFilename) const { + ai_assert(pFilename != nullptr); + + if (pFilename == nullptr) { + return false; + } + + std::string filename(pFilename); + return pImpl->Exists(filename); + } + + // This is always '/' in a ZIP + char ZipArchiveIOSystem::getOsSeparator() const { + return '/'; + } + + // Only supports Reading + IOStream * ZipArchiveIOSystem::Open(const char* pFilename, const char* pMode) { + ai_assert(pFilename != nullptr); + + for (size_t i = 0; pMode[i] != 0; ++i) + { + ai_assert(pMode[i] != 'w'); + if (pMode[i] == 'w') + return nullptr; + } + + std::string filename(pFilename); + return pImpl->OpenFile(filename); + } + + void ZipArchiveIOSystem::Close(IOStream* pFile) { + delete pFile; + } + + bool ZipArchiveIOSystem::isOpen() const { + return (pImpl->isOpen()); + } + + void ZipArchiveIOSystem::getFileList(std::vector& rFileList) const { + return pImpl->getFileList(rFileList); + } + + void ZipArchiveIOSystem::getFileListExtension(std::vector& rFileList, const std::string& extension) const { + return pImpl->getFileListExtension(rFileList, extension); + } + + bool ZipArchiveIOSystem::isZipArchive(IOSystem* pIOHandler, const char* pFilename) { + Implement tmp(pIOHandler, pFilename, "r"); + return tmp.isOpen(); + } + + bool ZipArchiveIOSystem::isZipArchive(IOSystem* pIOHandler, const std::string& rFilename) { + return isZipArchive(pIOHandler, rFilename.c_str()); + } + +} diff --git a/include/assimp/ZipArchiveIOSystem.h b/include/assimp/ZipArchiveIOSystem.h new file mode 100644 index 000000000..7be597f59 --- /dev/null +++ b/include/assimp/ZipArchiveIOSystem.h @@ -0,0 +1,40 @@ +#ifndef AI_ZIPARCHIVEIOSYSTEM_H_INC +#define AI_ZIPARCHIVEIOSYSTEM_H_INC + +#include +#include + +namespace Assimp { + class ZipArchiveIOSystem : public IOSystem { + public: + //! Open a Zip using the proffered IOSystem + ZipArchiveIOSystem(IOSystem* pIOHandler, const char *pFilename, const char* pMode = "r"); + ZipArchiveIOSystem(IOSystem* pIOHandler, const std::string& rFilename, const char* pMode = "r"); + virtual ~ZipArchiveIOSystem(); + bool Exists(const char* pFilename) const override; + char getOsSeparator() const override; + IOStream* Open(const char* pFilename, const char* pMode = "rb") override; + void Close(IOStream* pFile) override; + + // Specific to ZIP + //! The file was opened and is a ZIP + bool isOpen() const; + + //! Get the list of all files with their simplified paths + //! Intended for use within Assimp library boundaries + void getFileList(std::vector& rFileList) const; + + //! Get the list of all files with extension (must be lowercase) + //! Intended for use within Assimp library boundaries + void getFileListExtension(std::vector& rFileList, const std::string& extension) const; + + static bool isZipArchive(IOSystem* pIOHandler, const char *pFilename); + static bool isZipArchive(IOSystem* pIOHandler, const std::string& rFilename); + + private: + class Implement; + Implement *pImpl = nullptr; + }; +} // Namespace Assimp + +#endif // AI_ZIPARCHIVEIOSYSTEM_H_INC From d64e1bde1383b19f3c17084eeea9b63541605556 Mon Sep 17 00:00:00 2001 From: RichardTea <31507749+RichardTea@users.noreply.github.com> Date: Fri, 12 Jul 2019 11:29:35 +0100 Subject: [PATCH 02/19] First pass of Collada ZAE support Reads the manifest and loads the DAE Does not yet load embedded textures --- code/Collada/ColladaLoader.cpp | 1125 ++++++----- code/Collada/ColladaLoader.h | 2 +- code/Collada/ColladaParser.cpp | 2442 ++++++++++++----------- code/Collada/ColladaParser.h | 4 + test/models/Collada/duck.zae | Bin 0 -> 132801 bytes test/models/Collada/duck_nomanifest.zae | Bin 0 -> 132634 bytes 6 files changed, 1852 insertions(+), 1721 deletions(-) create mode 100644 test/models/Collada/duck.zae create mode 100644 test/models/Collada/duck_nomanifest.zae diff --git a/code/Collada/ColladaLoader.cpp b/code/Collada/ColladaLoader.cpp index 81db957d5..40b2b0811 100644 --- a/code/Collada/ColladaLoader.cpp +++ b/code/Collada/ColladaLoader.cpp @@ -60,6 +60,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include +#include #include "time.h" #include "math.h" @@ -75,30 +76,30 @@ static const aiImporterDesc desc = { "", "", "http://collada.org", - aiImporterFlags_SupportTextFlavour, + aiImporterFlags_SupportTextFlavour | aiImporterFlags_SupportCompressedFlavour, 1, 3, 1, 5, - "dae" + "dae zae" }; // ------------------------------------------------------------------------------------------------ // Constructor to be privately used by Importer ColladaLoader::ColladaLoader() -: mFileName() -, mMeshIndexByID() -, mMaterialIndexByName() -, mMeshes() -, newMats() -, mCameras() -, mLights() -, mTextures() -, mAnims() -, noSkeletonMesh( false ) -, ignoreUpDirection(false) -, useColladaName( false ) -, mNodeNameCounter( 0 ) { + : mFileName() + , mMeshIndexByID() + , mMaterialIndexByName() + , mMeshes() + , newMats() + , mCameras() + , mLights() + , mTextures() + , mAnims() + , noSkeletonMesh(false) + , ignoreUpDirection(false) + , useColladaName(false) + , mNodeNameCounter(0) { // empty } @@ -110,16 +111,27 @@ ColladaLoader::~ColladaLoader() { // ------------------------------------------------------------------------------------------------ // Returns whether the class can handle the format of the given file. -bool ColladaLoader::CanRead( const std::string& pFile, IOSystem* pIOHandler, bool checkSig) const { +bool ColladaLoader::CanRead(const std::string& pFile, IOSystem* pIOHandler, bool checkSig) const { // check file extension std::string extension = GetExtension(pFile); - if (extension == "dae") { - return true; + bool readSig = checkSig && (pIOHandler != nullptr); + + if (!readSig) { + if (extension == "dae" || extension == "zae") { + return true; + } + } + + if (readSig) { + // Look for a DAE file inside, but don't extract it + ZipArchiveIOSystem zip_archive(pIOHandler, pFile); + if (zip_archive.isOpen()) + return !ColladaParser::ReadZaeManifest(zip_archive).empty(); } // XML - too generic, we need to open the file and search for typical keywords - if( extension == "xml" || !extension.length() || checkSig) { + if (extension == "xml" || !extension.length() || checkSig) { /* If CanRead() is called in order to check whether we * support a specific file extension in general pIOHandler * might be NULL and it's our duty to return true here. @@ -127,8 +139,8 @@ bool ColladaLoader::CanRead( const std::string& pFile, IOSystem* pIOHandler, boo if (!pIOHandler) { return true; } - static const char* tokens[] = {"GetPropertyInteger(AI_CONFIG_IMPORT_NO_SKELETON_MESHES,0) != 0; - ignoreUpDirection = pImp->GetPropertyInteger(AI_CONFIG_IMPORT_COLLADA_IGNORE_UP_DIRECTION,0) != 0; - useColladaName = pImp->GetPropertyInteger(AI_CONFIG_IMPORT_COLLADA_USE_COLLADA_NAMES,0) != 0; + noSkeletonMesh = pImp->GetPropertyInteger(AI_CONFIG_IMPORT_NO_SKELETON_MESHES, 0) != 0; + ignoreUpDirection = pImp->GetPropertyInteger(AI_CONFIG_IMPORT_COLLADA_IGNORE_UP_DIRECTION, 0) != 0; + useColladaName = pImp->GetPropertyInteger(AI_CONFIG_IMPORT_COLLADA_USE_COLLADA_NAMES, 0) != 0; } // ------------------------------------------------------------------------------------------------ // Get file extension list -const aiImporterDesc* ColladaLoader::GetInfo () const { +const aiImporterDesc* ColladaLoader::GetInfo() const { return &desc; } // ------------------------------------------------------------------------------------------------ // Imports the given file into the given scene structure. -void ColladaLoader::InternReadFile( const std::string& pFile, aiScene* pScene, IOSystem* pIOHandler) { +void ColladaLoader::InternReadFile(const std::string& pFile, aiScene* pScene, IOSystem* pIOHandler) { mFileName = pFile; // clean all member arrays - just for safety, it should work even if we did not @@ -164,46 +176,46 @@ void ColladaLoader::InternReadFile( const std::string& pFile, aiScene* pScene, I mAnims.clear(); // parse the input file - ColladaParser parser( pIOHandler, pFile); + ColladaParser parser(pIOHandler, pFile); - if( !parser.mRootNode) - throw DeadlyImportError( "Collada: File came out empty. Something is wrong here."); + if (!parser.mRootNode) + throw DeadlyImportError("Collada: File came out empty. Something is wrong here."); // reserve some storage to avoid unnecessary reallocs - newMats.reserve(parser.mMaterialLibrary.size()*2); - mMeshes.reserve(parser.mMeshLibrary.size()*2); + newMats.reserve(parser.mMaterialLibrary.size() * 2); + mMeshes.reserve(parser.mMeshLibrary.size() * 2); mCameras.reserve(parser.mCameraLibrary.size()); mLights.reserve(parser.mLightLibrary.size()); // create the materials first, for the meshes to find - BuildMaterials( parser, pScene); + BuildMaterials(parser, pScene); // build the node hierarchy from it - pScene->mRootNode = BuildHierarchy( parser, parser.mRootNode); + pScene->mRootNode = BuildHierarchy(parser, parser.mRootNode); // ... then fill the materials with the now adjusted settings FillMaterials(parser, pScene); // Apply unit-size scale calculation - pScene->mRootNode->mTransformation *= aiMatrix4x4(parser.mUnitSize, 0, 0, 0, - 0, parser.mUnitSize, 0, 0, - 0, 0, parser.mUnitSize, 0, - 0, 0, 0, 1); - if( !ignoreUpDirection ) { - // Convert to Y_UP, if different orientation - if( parser.mUpDirection == ColladaParser::UP_X) - pScene->mRootNode->mTransformation *= aiMatrix4x4( - 0, -1, 0, 0, - 1, 0, 0, 0, - 0, 0, 1, 0, - 0, 0, 0, 1); - else if( parser.mUpDirection == ColladaParser::UP_Z) - pScene->mRootNode->mTransformation *= aiMatrix4x4( - 1, 0, 0, 0, - 0, 0, 1, 0, - 0, -1, 0, 0, - 0, 0, 0, 1); + pScene->mRootNode->mTransformation *= aiMatrix4x4(parser.mUnitSize, 0, 0, 0, + 0, parser.mUnitSize, 0, 0, + 0, 0, parser.mUnitSize, 0, + 0, 0, 0, 1); + if (!ignoreUpDirection) { + // Convert to Y_UP, if different orientation + if (parser.mUpDirection == ColladaParser::UP_X) + pScene->mRootNode->mTransformation *= aiMatrix4x4( + 0, -1, 0, 0, + 1, 0, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1); + else if (parser.mUpDirection == ColladaParser::UP_Z) + pScene->mRootNode->mTransformation *= aiMatrix4x4( + 1, 0, 0, 0, + 0, 0, 1, 0, + 0, -1, 0, 0, + 0, 0, 0, 1); } // Store scene metadata @@ -218,19 +230,19 @@ void ColladaLoader::InternReadFile( const std::string& pFile, aiScene* pScene, I } // store all meshes - StoreSceneMeshes( pScene); + StoreSceneMeshes(pScene); // store all materials - StoreSceneMaterials( pScene); + StoreSceneMaterials(pScene); // store all lights - StoreSceneLights( pScene); + StoreSceneLights(pScene); // store all cameras - StoreSceneCameras( pScene); + StoreSceneCameras(pScene); // store all animations - StoreAnimations( pScene, parser); + StoreAnimations(pScene, parser); // If no meshes have been loaded, it's probably just an animated skeleton. @@ -245,56 +257,56 @@ void ColladaLoader::InternReadFile( const std::string& pFile, aiScene* pScene, I // ------------------------------------------------------------------------------------------------ // Recursively constructs a scene node for the given parser node and returns it. -aiNode* ColladaLoader::BuildHierarchy( const ColladaParser& pParser, const Collada::Node* pNode) { +aiNode* ColladaLoader::BuildHierarchy(const ColladaParser& pParser, const Collada::Node* pNode) { // create a node for it aiNode* node = new aiNode(); // find a name for the new node. It's more complicated than you might think - node->mName.Set( FindNameForNode( pNode)); + node->mName.Set(FindNameForNode(pNode)); // calculate the transformation matrix for it - node->mTransformation = pParser.CalculateResultTransform( pNode->mTransforms); + node->mTransformation = pParser.CalculateResultTransform(pNode->mTransforms); // now resolve node instances std::vector instances; - ResolveNodeInstances(pParser,pNode,instances); + ResolveNodeInstances(pParser, pNode, instances); // add children. first the *real* ones - node->mNumChildren = static_cast(pNode->mChildren.size()+instances.size()); + node->mNumChildren = static_cast(pNode->mChildren.size() + instances.size()); node->mChildren = new aiNode*[node->mNumChildren]; - for( size_t a = 0; a < pNode->mChildren.size(); ++a) { - node->mChildren[a] = BuildHierarchy( pParser, pNode->mChildren[a]); + for (size_t a = 0; a < pNode->mChildren.size(); ++a) { + node->mChildren[a] = BuildHierarchy(pParser, pNode->mChildren[a]); node->mChildren[a]->mParent = node; } // ... and finally the resolved node instances - for( size_t a = 0; a < instances.size(); ++a) { - node->mChildren[pNode->mChildren.size() + a] = BuildHierarchy( pParser, instances[a]); + for (size_t a = 0; a < instances.size(); ++a) { + node->mChildren[pNode->mChildren.size() + a] = BuildHierarchy(pParser, instances[a]); node->mChildren[pNode->mChildren.size() + a]->mParent = node; } // construct meshes - BuildMeshesForNode( pParser, pNode, node); + BuildMeshesForNode(pParser, pNode, node); // construct cameras BuildCamerasForNode(pParser, pNode, node); // construct lights BuildLightsForNode(pParser, pNode, node); - + return node; } // ------------------------------------------------------------------------------------------------ // Resolve node instances -void ColladaLoader::ResolveNodeInstances( const ColladaParser& pParser, const Collada::Node* pNode, - std::vector& resolved) { +void ColladaLoader::ResolveNodeInstances(const ColladaParser& pParser, const Collada::Node* pNode, + std::vector& resolved) { // reserve enough storage resolved.reserve(pNode->mNodeInstances.size()); // ... and iterate through all nodes to be instanced as children of pNode - for (const auto &nodeInst: pNode->mNodeInstances) { + for (const auto &nodeInst : pNode->mNodeInstances) { // find the corresponding node in the library const ColladaParser::NodeLibrary::const_iterator itt = pParser.mNodeLibrary.find(nodeInst.mNode); const Collada::Node* nd = itt == pParser.mNodeLibrary.end() ? NULL : (*itt).second; @@ -318,7 +330,7 @@ void ColladaLoader::ResolveNodeInstances( const ColladaParser& pParser, const Co // ------------------------------------------------------------------------------------------------ // Resolve UV channels void ColladaLoader::ApplyVertexToEffectSemanticMapping(Collada::Sampler& sampler, - const Collada::SemanticMappingTable& table) { + const Collada::SemanticMappingTable& table) { std::map::const_iterator it = table.mMap.find(sampler.mUVChannel); if (it != table.mMap.end()) { if (it->second.mType != Collada::IT_Texcoord) { @@ -331,12 +343,12 @@ void ColladaLoader::ApplyVertexToEffectSemanticMapping(Collada::Sampler& sampler // ------------------------------------------------------------------------------------------------ // Builds lights for the given node and references them -void ColladaLoader::BuildLightsForNode( const ColladaParser& pParser, const Collada::Node* pNode, aiNode* pTarget) { - for( const Collada::LightInstance& lid : pNode->mLights) { +void ColladaLoader::BuildLightsForNode(const ColladaParser& pParser, const Collada::Node* pNode, aiNode* pTarget) { + for (const Collada::LightInstance& lid : pNode->mLights) { // find the referred light - ColladaParser::LightLibrary::const_iterator srcLightIt = pParser.mLightLibrary.find( lid.mLight); - if( srcLightIt == pParser.mLightLibrary.end()) { - ASSIMP_LOG_WARN_F("Collada: Unable to find light for ID \"" , lid.mLight , "\". Skipping."); + ColladaParser::LightLibrary::const_iterator srcLightIt = pParser.mLightLibrary.find(lid.mLight); + if (srcLightIt == pParser.mLightLibrary.end()) { + ASSIMP_LOG_WARN_F("Collada: Unable to find light for ID \"", lid.mLight, "\". Skipping."); continue; } const Collada::Light* srcLight = &srcLightIt->second; @@ -347,7 +359,7 @@ void ColladaLoader::BuildLightsForNode( const ColladaParser& pParser, const Coll out->mType = (aiLightSourceType)srcLight->mType; // collada lights point in -Z by default, rest is specified in node transform - out->mDirection = aiVector3D(0.f,0.f,-1.f); + out->mDirection = aiVector3D(0.f, 0.f, -1.f); out->mAttenuationConstant = srcLight->mAttConstant; out->mAttenuationLinear = srcLight->mAttLinear; @@ -357,7 +369,8 @@ void ColladaLoader::BuildLightsForNode( const ColladaParser& pParser, const Coll if (out->mType == aiLightSource_AMBIENT) { out->mColorDiffuse = out->mColorSpecular = aiColor3D(0, 0, 0); out->mColorAmbient = srcLight->mColor*srcLight->mIntensity; - } else { + } + else { // collada doesn't differentiate between these color types out->mColorDiffuse = out->mColorSpecular = srcLight->mColor*srcLight->mIntensity; out->mColorAmbient = aiColor3D(0, 0, 0); @@ -365,23 +378,25 @@ void ColladaLoader::BuildLightsForNode( const ColladaParser& pParser, const Coll // convert falloff angle and falloff exponent in our representation, if given if (out->mType == aiLightSource_SPOT) { - out->mAngleInnerCone = AI_DEG_TO_RAD( srcLight->mFalloffAngle ); + out->mAngleInnerCone = AI_DEG_TO_RAD(srcLight->mFalloffAngle); // ... some extension magic. - if (srcLight->mOuterAngle >= ASSIMP_COLLADA_LIGHT_ANGLE_NOT_SET*(1-1e-6f)) { + if (srcLight->mOuterAngle >= ASSIMP_COLLADA_LIGHT_ANGLE_NOT_SET * (1 - 1e-6f)) { // ... some deprecation magic. - if (srcLight->mPenumbraAngle >= ASSIMP_COLLADA_LIGHT_ANGLE_NOT_SET*(1-1e-6f)) { + if (srcLight->mPenumbraAngle >= ASSIMP_COLLADA_LIGHT_ANGLE_NOT_SET * (1 - 1e-6f)) { // Need to rely on falloff_exponent. I don't know how to interpret it, so I need to guess .... // epsilon chosen to be 0.1 - out->mAngleOuterCone = std::acos(std::pow(0.1f,1.f/srcLight->mFalloffExponent))+ - out->mAngleInnerCone; - } else { - out->mAngleOuterCone = out->mAngleInnerCone + AI_DEG_TO_RAD( srcLight->mPenumbraAngle ); - if (out->mAngleOuterCone < out->mAngleInnerCone) - std::swap(out->mAngleInnerCone,out->mAngleOuterCone); + out->mAngleOuterCone = std::acos(std::pow(0.1f, 1.f / srcLight->mFalloffExponent)) + + out->mAngleInnerCone; } - } else { - out->mAngleOuterCone = AI_DEG_TO_RAD( srcLight->mOuterAngle ); + else { + out->mAngleOuterCone = out->mAngleInnerCone + AI_DEG_TO_RAD(srcLight->mPenumbraAngle); + if (out->mAngleOuterCone < out->mAngleInnerCone) + std::swap(out->mAngleInnerCone, out->mAngleOuterCone); + } + } + else { + out->mAngleOuterCone = AI_DEG_TO_RAD(srcLight->mOuterAngle); } } @@ -392,12 +407,12 @@ void ColladaLoader::BuildLightsForNode( const ColladaParser& pParser, const Coll // ------------------------------------------------------------------------------------------------ // Builds cameras for the given node and references them -void ColladaLoader::BuildCamerasForNode( const ColladaParser& pParser, const Collada::Node* pNode, aiNode* pTarget) { - for( const Collada::CameraInstance& cid : pNode->mCameras) { +void ColladaLoader::BuildCamerasForNode(const ColladaParser& pParser, const Collada::Node* pNode, aiNode* pTarget) { + for (const Collada::CameraInstance& cid : pNode->mCameras) { // find the referred light - ColladaParser::CameraLibrary::const_iterator srcCameraIt = pParser.mCameraLibrary.find( cid.mCamera); - if( srcCameraIt == pParser.mCameraLibrary.end()) { - ASSIMP_LOG_WARN_F("Collada: Unable to find camera for ID \"" , cid.mCamera , "\". Skipping."); + ColladaParser::CameraLibrary::const_iterator srcCameraIt = pParser.mCameraLibrary.find(cid.mCamera); + if (srcCameraIt == pParser.mCameraLibrary.end()) { + ASSIMP_LOG_WARN_F("Collada: Unable to find camera for ID \"", cid.mCamera, "\". Skipping."); continue; } const Collada::Camera* srcCamera = &srcCameraIt->second; @@ -412,7 +427,7 @@ void ColladaLoader::BuildCamerasForNode( const ColladaParser& pParser, const Col out->mName = pTarget->mName; // collada cameras point in -Z by default, rest is specified in node transform - out->mLookAt = aiVector3D(0.f,0.f,-1.f); + out->mLookAt = aiVector3D(0.f, 0.f, -1.f); // near/far z is already ok out->mClipPlaneFar = srcCamera->mZFar; @@ -432,7 +447,7 @@ void ColladaLoader::BuildCamerasForNode( const ColladaParser& pParser, const Col std::tan(AI_DEG_TO_RAD(srcCamera->mVerFov)); } } - else if (srcCamera->mAspect != 10e10f && srcCamera->mVerFov != 10e10f) { + else if (srcCamera->mAspect != 10e10f && srcCamera->mVerFov != 10e10f) { out->mHorizontalFOV = 2.0f * AI_RAD_TO_DEG(std::atan(srcCamera->mAspect * std::tan(AI_DEG_TO_RAD(srcCamera->mVerFov) * 0.5f))); } @@ -447,103 +462,106 @@ void ColladaLoader::BuildCamerasForNode( const ColladaParser& pParser, const Col // ------------------------------------------------------------------------------------------------ // Builds meshes for the given node and references them -void ColladaLoader::BuildMeshesForNode( const ColladaParser& pParser, const Collada::Node* pNode, aiNode* pTarget) { +void ColladaLoader::BuildMeshesForNode(const ColladaParser& pParser, const Collada::Node* pNode, aiNode* pTarget) { // accumulated mesh references by this node std::vector newMeshRefs; newMeshRefs.reserve(pNode->mMeshes.size()); // add a mesh for each subgroup in each collada mesh - for( const Collada::MeshInstance& mid : pNode->mMeshes) { + for (const Collada::MeshInstance& mid : pNode->mMeshes) { const Collada::Mesh* srcMesh = nullptr; const Collada::Controller* srcController = nullptr; // find the referred mesh - ColladaParser::MeshLibrary::const_iterator srcMeshIt = pParser.mMeshLibrary.find( mid.mMeshOrController); - if( srcMeshIt == pParser.mMeshLibrary.end()) { + ColladaParser::MeshLibrary::const_iterator srcMeshIt = pParser.mMeshLibrary.find(mid.mMeshOrController); + if (srcMeshIt == pParser.mMeshLibrary.end()) { // if not found in the mesh-library, it might also be a controller referring to a mesh - ColladaParser::ControllerLibrary::const_iterator srcContrIt = pParser.mControllerLibrary.find( mid.mMeshOrController); - if( srcContrIt != pParser.mControllerLibrary.end()) { + ColladaParser::ControllerLibrary::const_iterator srcContrIt = pParser.mControllerLibrary.find(mid.mMeshOrController); + if (srcContrIt != pParser.mControllerLibrary.end()) { srcController = &srcContrIt->second; - srcMeshIt = pParser.mMeshLibrary.find( srcController->mMeshId); - if( srcMeshIt != pParser.mMeshLibrary.end()) { + srcMeshIt = pParser.mMeshLibrary.find(srcController->mMeshId); + if (srcMeshIt != pParser.mMeshLibrary.end()) { srcMesh = srcMeshIt->second; } } - if( !srcMesh) { - ASSIMP_LOG_WARN_F( "Collada: Unable to find geometry for ID \"", mid.mMeshOrController, "\". Skipping." ); + if (!srcMesh) { + ASSIMP_LOG_WARN_F("Collada: Unable to find geometry for ID \"", mid.mMeshOrController, "\". Skipping."); continue; } - } else { + } + else { // ID found in the mesh library -> direct reference to an unskinned mesh srcMesh = srcMeshIt->second; } // build a mesh for each of its subgroups size_t vertexStart = 0, faceStart = 0; - for( size_t sm = 0; sm < srcMesh->mSubMeshes.size(); ++sm) { + for (size_t sm = 0; sm < srcMesh->mSubMeshes.size(); ++sm) { const Collada::SubMesh& submesh = srcMesh->mSubMeshes[sm]; - if( submesh.mNumFaces == 0) { + if (submesh.mNumFaces == 0) { continue; } // find material assigned to this submesh std::string meshMaterial; - std::map::const_iterator meshMatIt = mid.mMaterials.find( submesh.mMaterial); + std::map::const_iterator meshMatIt = mid.mMaterials.find(submesh.mMaterial); const Collada::SemanticMappingTable* table = nullptr; - if( meshMatIt != mid.mMaterials.end()) { + if (meshMatIt != mid.mMaterials.end()) { table = &meshMatIt->second; meshMaterial = table->mMatName; - } else { - ASSIMP_LOG_WARN_F( "Collada: No material specified for subgroup <", submesh.mMaterial, "> in geometry <", - mid.mMeshOrController, ">." ); - if( !mid.mMaterials.empty() ) { + } + else { + ASSIMP_LOG_WARN_F("Collada: No material specified for subgroup <", submesh.mMaterial, "> in geometry <", + mid.mMeshOrController, ">."); + if (!mid.mMaterials.empty()) { meshMaterial = mid.mMaterials.begin()->second.mMatName; } } // OK ... here the *real* fun starts ... we have the vertex-input-to-effect-semantic-table // given. The only mapping stuff which we do actually support is the UV channel. - std::map::const_iterator matIt = mMaterialIndexByName.find( meshMaterial); + std::map::const_iterator matIt = mMaterialIndexByName.find(meshMaterial); unsigned int matIdx = 0; - if( matIt != mMaterialIndexByName.end()) { + if (matIt != mMaterialIndexByName.end()) { matIdx = static_cast(matIt->second); } - if (table && !table->mMap.empty() ) { + if (table && !table->mMap.empty()) { std::pair& mat = newMats[matIdx]; // Iterate through all texture channels assigned to the effect and // check whether we have mapping information for it. - ApplyVertexToEffectSemanticMapping(mat.first->mTexDiffuse, *table); - ApplyVertexToEffectSemanticMapping(mat.first->mTexAmbient, *table); - ApplyVertexToEffectSemanticMapping(mat.first->mTexSpecular, *table); - ApplyVertexToEffectSemanticMapping(mat.first->mTexEmissive, *table); - ApplyVertexToEffectSemanticMapping(mat.first->mTexTransparent,*table); - ApplyVertexToEffectSemanticMapping(mat.first->mTexBump, *table); + ApplyVertexToEffectSemanticMapping(mat.first->mTexDiffuse, *table); + ApplyVertexToEffectSemanticMapping(mat.first->mTexAmbient, *table); + ApplyVertexToEffectSemanticMapping(mat.first->mTexSpecular, *table); + ApplyVertexToEffectSemanticMapping(mat.first->mTexEmissive, *table); + ApplyVertexToEffectSemanticMapping(mat.first->mTexTransparent, *table); + ApplyVertexToEffectSemanticMapping(mat.first->mTexBump, *table); } // built lookup index of the Mesh-Submesh-Material combination - ColladaMeshIndex index( mid.mMeshOrController, sm, meshMaterial); + ColladaMeshIndex index(mid.mMeshOrController, sm, meshMaterial); // if we already have the mesh at the library, just add its index to the node's array - std::map::const_iterator dstMeshIt = mMeshIndexByID.find( index); - if( dstMeshIt != mMeshIndexByID.end()) { - newMeshRefs.push_back( dstMeshIt->second); - } else { + std::map::const_iterator dstMeshIt = mMeshIndexByID.find(index); + if (dstMeshIt != mMeshIndexByID.end()) { + newMeshRefs.push_back(dstMeshIt->second); + } + else { // else we have to add the mesh to the collection and store its newly assigned index at the node - aiMesh* dstMesh = CreateMesh( pParser, srcMesh, submesh, srcController, vertexStart, faceStart); + aiMesh* dstMesh = CreateMesh(pParser, srcMesh, submesh, srcController, vertexStart, faceStart); // store the mesh, and store its new index in the node - newMeshRefs.push_back( mMeshes.size()); + newMeshRefs.push_back(mMeshes.size()); mMeshIndexByID[index] = mMeshes.size(); - mMeshes.push_back( dstMesh); + mMeshes.push_back(dstMesh); vertexStart += dstMesh->mNumVertices; faceStart += submesh.mNumFaces; // assign the material index dstMesh->mMaterialIndex = matIdx; - if(dstMesh->mName.length == 0) { + if (dstMesh->mName.length == 0) { dstMesh->mName = mid.mMeshOrController; } } @@ -552,7 +570,7 @@ void ColladaLoader::BuildMeshesForNode( const ColladaParser& pParser, const Coll // now place all mesh references we gathered in the target node pTarget->mNumMeshes = static_cast(newMeshRefs.size()); - if( newMeshRefs.size()) { + if (newMeshRefs.size()) { struct UIntTypeConverter { unsigned int operator()(const size_t& v) const { return static_cast(v); @@ -560,76 +578,76 @@ void ColladaLoader::BuildMeshesForNode( const ColladaParser& pParser, const Coll }; pTarget->mMeshes = new unsigned int[pTarget->mNumMeshes]; - std::transform( newMeshRefs.begin(), newMeshRefs.end(), pTarget->mMeshes, UIntTypeConverter()); + std::transform(newMeshRefs.begin(), newMeshRefs.end(), pTarget->mMeshes, UIntTypeConverter()); } } // ------------------------------------------------------------------------------------------------ // Find mesh from either meshes or morph target meshes aiMesh *ColladaLoader::findMesh(std::string meshid) { - for (unsigned int i = 0; i < mMeshes.size(); ++i ) { + for (unsigned int i = 0; i < mMeshes.size(); ++i) { if (std::string(mMeshes[i]->mName.data) == meshid) { return mMeshes[i]; } } - for (unsigned int i = 0; i < mTargetMeshes.size(); ++i ) { + for (unsigned int i = 0; i < mTargetMeshes.size(); ++i) { if (std::string(mTargetMeshes[i]->mName.data) == meshid) { return mTargetMeshes[i]; } } - + return nullptr; } // ------------------------------------------------------------------------------------------------ // Creates a mesh for the given ColladaMesh face subset and returns the newly created mesh -aiMesh* ColladaLoader::CreateMesh( const ColladaParser& pParser, const Collada::Mesh* pSrcMesh, const Collada::SubMesh& pSubMesh, - const Collada::Controller* pSrcController, size_t pStartVertex, size_t pStartFace) { +aiMesh* ColladaLoader::CreateMesh(const ColladaParser& pParser, const Collada::Mesh* pSrcMesh, const Collada::SubMesh& pSubMesh, + const Collada::Controller* pSrcController, size_t pStartVertex, size_t pStartFace) { std::unique_ptr dstMesh(new aiMesh); dstMesh->mName = pSrcMesh->mName; // count the vertices addressed by its faces - const size_t numVertices = std::accumulate( pSrcMesh->mFaceSize.begin() + pStartFace, + const size_t numVertices = std::accumulate(pSrcMesh->mFaceSize.begin() + pStartFace, pSrcMesh->mFaceSize.begin() + pStartFace + pSubMesh.mNumFaces, size_t(0)); // copy positions dstMesh->mNumVertices = static_cast(numVertices); dstMesh->mVertices = new aiVector3D[numVertices]; - std::copy( pSrcMesh->mPositions.begin() + pStartVertex, pSrcMesh->mPositions.begin() + + std::copy(pSrcMesh->mPositions.begin() + pStartVertex, pSrcMesh->mPositions.begin() + pStartVertex + numVertices, dstMesh->mVertices); // normals, if given. HACK: (thom) Due to the glorious Collada spec we never // know if we have the same number of normals as there are positions. So we // also ignore any vertex attribute if it has a different count - if( pSrcMesh->mNormals.size() >= pStartVertex + numVertices) { + if (pSrcMesh->mNormals.size() >= pStartVertex + numVertices) { dstMesh->mNormals = new aiVector3D[numVertices]; - std::copy( pSrcMesh->mNormals.begin() + pStartVertex, pSrcMesh->mNormals.begin() + + std::copy(pSrcMesh->mNormals.begin() + pStartVertex, pSrcMesh->mNormals.begin() + pStartVertex + numVertices, dstMesh->mNormals); } // tangents, if given. - if( pSrcMesh->mTangents.size() >= pStartVertex + numVertices) { + if (pSrcMesh->mTangents.size() >= pStartVertex + numVertices) { dstMesh->mTangents = new aiVector3D[numVertices]; - std::copy( pSrcMesh->mTangents.begin() + pStartVertex, pSrcMesh->mTangents.begin() + + std::copy(pSrcMesh->mTangents.begin() + pStartVertex, pSrcMesh->mTangents.begin() + pStartVertex + numVertices, dstMesh->mTangents); } // bitangents, if given. - if( pSrcMesh->mBitangents.size() >= pStartVertex + numVertices) { + if (pSrcMesh->mBitangents.size() >= pStartVertex + numVertices) { dstMesh->mBitangents = new aiVector3D[numVertices]; - std::copy( pSrcMesh->mBitangents.begin() + pStartVertex, pSrcMesh->mBitangents.begin() + + std::copy(pSrcMesh->mBitangents.begin() + pStartVertex, pSrcMesh->mBitangents.begin() + pStartVertex + numVertices, dstMesh->mBitangents); } // same for texturecoords, as many as we have // empty slots are not allowed, need to pack and adjust UV indexes accordingly - for( size_t a = 0, real = 0; a < AI_MAX_NUMBER_OF_TEXTURECOORDS; ++a ) { - if( pSrcMesh->mTexCoords[a].size() >= pStartVertex + numVertices) { + for (size_t a = 0, real = 0; a < AI_MAX_NUMBER_OF_TEXTURECOORDS; ++a) { + if (pSrcMesh->mTexCoords[a].size() >= pStartVertex + numVertices) { dstMesh->mTextureCoords[real] = new aiVector3D[numVertices]; - for( size_t b = 0; b < numVertices; ++b) { - dstMesh->mTextureCoords[real][b] = pSrcMesh->mTexCoords[a][pStartVertex+b]; + for (size_t b = 0; b < numVertices; ++b) { + dstMesh->mTextureCoords[real][b] = pSrcMesh->mTexCoords[a][pStartVertex + b]; } dstMesh->mNumUVComponents[real] = pSrcMesh->mNumUVComponents[a]; @@ -638,10 +656,10 @@ aiMesh* ColladaLoader::CreateMesh( const ColladaParser& pParser, const Collada:: } // same for vertex colors, as many as we have. again the same packing to avoid empty slots - for( size_t a = 0, real = 0; a < AI_MAX_NUMBER_OF_COLOR_SETS; ++a ) { - if( pSrcMesh->mColors[a].size() >= pStartVertex + numVertices) { + for (size_t a = 0, real = 0; a < AI_MAX_NUMBER_OF_COLOR_SETS; ++a) { + if (pSrcMesh->mColors[a].size() >= pStartVertex + numVertices) { dstMesh->mColors[real] = new aiColor4D[numVertices]; - std::copy( pSrcMesh->mColors[a].begin() + pStartVertex, pSrcMesh->mColors[a].begin() + pStartVertex + numVertices,dstMesh->mColors[real]); + std::copy(pSrcMesh->mColors[a].begin() + pStartVertex, pSrcMesh->mColors[a].begin() + pStartVertex + numVertices, dstMesh->mColors[real]); ++real; } } @@ -650,12 +668,12 @@ aiMesh* ColladaLoader::CreateMesh( const ColladaParser& pParser, const Collada:: size_t vertex = 0; dstMesh->mNumFaces = static_cast(pSubMesh.mNumFaces); dstMesh->mFaces = new aiFace[dstMesh->mNumFaces]; - for( size_t a = 0; a < dstMesh->mNumFaces; ++a) { - size_t s = pSrcMesh->mFaceSize[ pStartFace + a]; + for (size_t a = 0; a < dstMesh->mNumFaces; ++a) { + size_t s = pSrcMesh->mFaceSize[pStartFace + a]; aiFace& face = dstMesh->mFaces[a]; face.mNumIndices = static_cast(s); face.mIndices = new unsigned int[s]; - for( size_t b = 0; b < s; ++b) { + for (size_t b = 0; b < s; ++b) { face.mIndices[b] = static_cast(vertex++); } } @@ -665,25 +683,25 @@ aiMesh* ColladaLoader::CreateMesh( const ColladaParser& pParser, const Collada:: std::vector targetWeights; Collada::MorphMethod method = Collada::Normalized; - for(std::map::const_iterator it = pParser.mControllerLibrary.begin(); - it != pParser.mControllerLibrary.end(); it++) { + for (std::map::const_iterator it = pParser.mControllerLibrary.begin(); + it != pParser.mControllerLibrary.end(); it++) { const Collada::Controller &c = it->second; - const Collada::Mesh* baseMesh = pParser.ResolveLibraryReference( pParser.mMeshLibrary, c.mMeshId); + const Collada::Mesh* baseMesh = pParser.ResolveLibraryReference(pParser.mMeshLibrary, c.mMeshId); if (c.mType == Collada::Morph && baseMesh->mName == pSrcMesh->mName) { - const Collada::Accessor& targetAccessor = pParser.ResolveLibraryReference( pParser.mAccessorLibrary, c.mMorphTarget); - const Collada::Accessor& weightAccessor = pParser.ResolveLibraryReference( pParser.mAccessorLibrary, c.mMorphWeight); - const Collada::Data& targetData = pParser.ResolveLibraryReference( pParser.mDataLibrary, targetAccessor.mSource); - const Collada::Data& weightData = pParser.ResolveLibraryReference( pParser.mDataLibrary, weightAccessor.mSource); + const Collada::Accessor& targetAccessor = pParser.ResolveLibraryReference(pParser.mAccessorLibrary, c.mMorphTarget); + const Collada::Accessor& weightAccessor = pParser.ResolveLibraryReference(pParser.mAccessorLibrary, c.mMorphWeight); + const Collada::Data& targetData = pParser.ResolveLibraryReference(pParser.mDataLibrary, targetAccessor.mSource); + const Collada::Data& weightData = pParser.ResolveLibraryReference(pParser.mDataLibrary, weightAccessor.mSource); // take method method = c.mMethod; if (!targetData.mIsStringArray) { - throw DeadlyImportError( "target data must contain id. "); + throw DeadlyImportError("target data must contain id. "); } if (weightData.mIsStringArray) { - throw DeadlyImportError( "target weight data must not be textual "); + throw DeadlyImportError("target weight data must not be textual "); } for (unsigned int i = 0; i < targetData.mStrings.size(); ++i) { @@ -692,7 +710,7 @@ aiMesh* ColladaLoader::CreateMesh( const ColladaParser& pParser, const Collada:: aiMesh *aimesh = findMesh(targetMesh->mName); if (!aimesh) { if (targetMesh->mSubMeshes.size() > 1) { - throw DeadlyImportError( "Morhing target mesh must be a single"); + throw DeadlyImportError("Morhing target mesh must be a single"); } aimesh = CreateMesh(pParser, targetMesh, targetMesh->mSubMeshes.at(0), NULL, 0, 0); mTargetMeshes.push_back(aimesh); @@ -706,7 +724,7 @@ aiMesh* ColladaLoader::CreateMesh( const ColladaParser& pParser, const Collada:: } if (targetMeshes.size() > 0 && targetWeights.size() == targetMeshes.size()) { std::vector animMeshes; - for (unsigned int i = 0; i < targetMeshes.size(); ++i ) { + for (unsigned int i = 0; i < targetMeshes.size(); ++i) { aiMesh* targetMesh = targetMeshes.at(i); aiAnimMesh *animMesh = aiCreateAnimMesh(targetMesh); float weight = targetWeights[i]; @@ -715,54 +733,54 @@ aiMesh* ColladaLoader::CreateMesh( const ColladaParser& pParser, const Collada:: animMeshes.push_back(animMesh); } dstMesh->mMethod = (method == Collada::Relative) - ? aiMorphingMethod_MORPH_RELATIVE - : aiMorphingMethod_MORPH_NORMALIZED; + ? aiMorphingMethod_MORPH_RELATIVE + : aiMorphingMethod_MORPH_NORMALIZED; dstMesh->mAnimMeshes = new aiAnimMesh*[animMeshes.size()]; dstMesh->mNumAnimMeshes = static_cast(animMeshes.size()); - for (unsigned int i = 0; i < animMeshes.size(); ++i ) { + for (unsigned int i = 0; i < animMeshes.size(); ++i) { dstMesh->mAnimMeshes[i] = animMeshes.at(i); } } // create bones if given - if( pSrcController && pSrcController->mType == Collada::Skin) { + if (pSrcController && pSrcController->mType == Collada::Skin) { // resolve references - joint names - const Collada::Accessor& jointNamesAcc = pParser.ResolveLibraryReference( pParser.mAccessorLibrary, pSrcController->mJointNameSource); - const Collada::Data& jointNames = pParser.ResolveLibraryReference( pParser.mDataLibrary, jointNamesAcc.mSource); + const Collada::Accessor& jointNamesAcc = pParser.ResolveLibraryReference(pParser.mAccessorLibrary, pSrcController->mJointNameSource); + const Collada::Data& jointNames = pParser.ResolveLibraryReference(pParser.mDataLibrary, jointNamesAcc.mSource); // joint offset matrices - const Collada::Accessor& jointMatrixAcc = pParser.ResolveLibraryReference( pParser.mAccessorLibrary, pSrcController->mJointOffsetMatrixSource); - const Collada::Data& jointMatrices = pParser.ResolveLibraryReference( pParser.mDataLibrary, jointMatrixAcc.mSource); + const Collada::Accessor& jointMatrixAcc = pParser.ResolveLibraryReference(pParser.mAccessorLibrary, pSrcController->mJointOffsetMatrixSource); + const Collada::Data& jointMatrices = pParser.ResolveLibraryReference(pParser.mDataLibrary, jointMatrixAcc.mSource); // joint vertex_weight name list - should refer to the same list as the joint names above. If not, report and reconsider - const Collada::Accessor& weightNamesAcc = pParser.ResolveLibraryReference( pParser.mAccessorLibrary, pSrcController->mWeightInputJoints.mAccessor); - if( &weightNamesAcc != &jointNamesAcc) - throw DeadlyImportError( "Temporary implementational laziness. If you read this, please report to the author."); + const Collada::Accessor& weightNamesAcc = pParser.ResolveLibraryReference(pParser.mAccessorLibrary, pSrcController->mWeightInputJoints.mAccessor); + if (&weightNamesAcc != &jointNamesAcc) + throw DeadlyImportError("Temporary implementational laziness. If you read this, please report to the author."); // vertex weights - const Collada::Accessor& weightsAcc = pParser.ResolveLibraryReference( pParser.mAccessorLibrary, pSrcController->mWeightInputWeights.mAccessor); - const Collada::Data& weights = pParser.ResolveLibraryReference( pParser.mDataLibrary, weightsAcc.mSource); + const Collada::Accessor& weightsAcc = pParser.ResolveLibraryReference(pParser.mAccessorLibrary, pSrcController->mWeightInputWeights.mAccessor); + const Collada::Data& weights = pParser.ResolveLibraryReference(pParser.mDataLibrary, weightsAcc.mSource); - if( !jointNames.mIsStringArray || jointMatrices.mIsStringArray || weights.mIsStringArray) - throw DeadlyImportError( "Data type mismatch while resolving mesh joints"); + if (!jointNames.mIsStringArray || jointMatrices.mIsStringArray || weights.mIsStringArray) + throw DeadlyImportError("Data type mismatch while resolving mesh joints"); // sanity check: we rely on the vertex weights always coming as pairs of BoneIndex-WeightIndex - if( pSrcController->mWeightInputJoints.mOffset != 0 || pSrcController->mWeightInputWeights.mOffset != 1) - throw DeadlyImportError( "Unsupported vertex_weight addressing scheme. "); + if (pSrcController->mWeightInputJoints.mOffset != 0 || pSrcController->mWeightInputWeights.mOffset != 1) + throw DeadlyImportError("Unsupported vertex_weight addressing scheme. "); // create containers to collect the weights for each bone size_t numBones = jointNames.mStrings.size(); - std::vector > dstBones( numBones); + std::vector > dstBones(numBones); // build a temporary array of pointers to the start of each vertex's weights typedef std::vector< std::pair > IndexPairVector; std::vector weightStartPerVertex; - weightStartPerVertex.resize(pSrcController->mWeightCounts.size(),pSrcController->mWeights.end()); + weightStartPerVertex.resize(pSrcController->mWeightCounts.size(), pSrcController->mWeights.end()); IndexPairVector::const_iterator pit = pSrcController->mWeights.begin(); - for( size_t a = 0; a < pSrcController->mWeightCounts.size(); ++a) { + for (size_t a = 0; a < pSrcController->mWeightCounts.size(); ++a) { weightStartPerVertex[a] = pit; pit += pSrcController->mWeightCounts[a]; } // now for each vertex put the corresponding vertex weights into each bone's weight collection - for( size_t a = pStartVertex; a < pStartVertex + numVertices; ++a) { + for (size_t a = pStartVertex; a < pStartVertex + numVertices; ++a) { // which position index was responsible for this vertex? that's also the index by which // the controller assigns the vertex weights size_t orgIndex = pSrcMesh->mFacePosIndices[a]; @@ -770,58 +788,58 @@ aiMesh* ColladaLoader::CreateMesh( const ColladaParser& pParser, const Collada:: IndexPairVector::const_iterator iit = weightStartPerVertex[orgIndex]; size_t pairCount = pSrcController->mWeightCounts[orgIndex]; - for( size_t b = 0; b < pairCount; ++b, ++iit) + for (size_t b = 0; b < pairCount; ++b, ++iit) { size_t jointIndex = iit->first; size_t vertexIndex = iit->second; - ai_real weight = ReadFloat( weightsAcc, weights, vertexIndex, 0); + ai_real weight = ReadFloat(weightsAcc, weights, vertexIndex, 0); // one day I gonna kill that XSI Collada exporter - if( weight > 0.0f) + if (weight > 0.0f) { aiVertexWeight w; w.mVertexId = static_cast(a - pStartVertex); w.mWeight = weight; - dstBones[jointIndex].push_back( w); + dstBones[jointIndex].push_back(w); } } } // count the number of bones which influence vertices of the current submesh size_t numRemainingBones = 0; - for( std::vector >::const_iterator it = dstBones.begin(); it != dstBones.end(); ++it) - if( it->size() > 0) + for (std::vector >::const_iterator it = dstBones.begin(); it != dstBones.end(); ++it) + if (it->size() > 0) numRemainingBones++; // create bone array and copy bone weights one by one dstMesh->mNumBones = static_cast(numRemainingBones); dstMesh->mBones = new aiBone*[numRemainingBones]; size_t boneCount = 0; - for( size_t a = 0; a < numBones; ++a) + for (size_t a = 0; a < numBones; ++a) { // omit bones without weights - if( dstBones[a].size() == 0) + if (dstBones[a].size() == 0) continue; // create bone with its weights aiBone* bone = new aiBone; - bone->mName = ReadString( jointNamesAcc, jointNames, a); - bone->mOffsetMatrix.a1 = ReadFloat( jointMatrixAcc, jointMatrices, a, 0); - bone->mOffsetMatrix.a2 = ReadFloat( jointMatrixAcc, jointMatrices, a, 1); - bone->mOffsetMatrix.a3 = ReadFloat( jointMatrixAcc, jointMatrices, a, 2); - bone->mOffsetMatrix.a4 = ReadFloat( jointMatrixAcc, jointMatrices, a, 3); - bone->mOffsetMatrix.b1 = ReadFloat( jointMatrixAcc, jointMatrices, a, 4); - bone->mOffsetMatrix.b2 = ReadFloat( jointMatrixAcc, jointMatrices, a, 5); - bone->mOffsetMatrix.b3 = ReadFloat( jointMatrixAcc, jointMatrices, a, 6); - bone->mOffsetMatrix.b4 = ReadFloat( jointMatrixAcc, jointMatrices, a, 7); - bone->mOffsetMatrix.c1 = ReadFloat( jointMatrixAcc, jointMatrices, a, 8); - bone->mOffsetMatrix.c2 = ReadFloat( jointMatrixAcc, jointMatrices, a, 9); - bone->mOffsetMatrix.c3 = ReadFloat( jointMatrixAcc, jointMatrices, a, 10); - bone->mOffsetMatrix.c4 = ReadFloat( jointMatrixAcc, jointMatrices, a, 11); + bone->mName = ReadString(jointNamesAcc, jointNames, a); + bone->mOffsetMatrix.a1 = ReadFloat(jointMatrixAcc, jointMatrices, a, 0); + bone->mOffsetMatrix.a2 = ReadFloat(jointMatrixAcc, jointMatrices, a, 1); + bone->mOffsetMatrix.a3 = ReadFloat(jointMatrixAcc, jointMatrices, a, 2); + bone->mOffsetMatrix.a4 = ReadFloat(jointMatrixAcc, jointMatrices, a, 3); + bone->mOffsetMatrix.b1 = ReadFloat(jointMatrixAcc, jointMatrices, a, 4); + bone->mOffsetMatrix.b2 = ReadFloat(jointMatrixAcc, jointMatrices, a, 5); + bone->mOffsetMatrix.b3 = ReadFloat(jointMatrixAcc, jointMatrices, a, 6); + bone->mOffsetMatrix.b4 = ReadFloat(jointMatrixAcc, jointMatrices, a, 7); + bone->mOffsetMatrix.c1 = ReadFloat(jointMatrixAcc, jointMatrices, a, 8); + bone->mOffsetMatrix.c2 = ReadFloat(jointMatrixAcc, jointMatrices, a, 9); + bone->mOffsetMatrix.c3 = ReadFloat(jointMatrixAcc, jointMatrices, a, 10); + bone->mOffsetMatrix.c4 = ReadFloat(jointMatrixAcc, jointMatrices, a, 11); bone->mNumWeights = static_cast(dstBones[a].size()); bone->mWeights = new aiVertexWeight[bone->mNumWeights]; - std::copy( dstBones[a].begin(), dstBones[a].end(), bone->mWeights); + std::copy(dstBones[a].begin(), dstBones[a].end(), bone->mWeights); // apply bind shape matrix to offset matrix aiMatrix4x4 bindShapeMatrix; @@ -847,15 +865,15 @@ aiMesh* ColladaLoader::CreateMesh( const ColladaParser& pParser, const Collada:: // Therefore I added a little name replacement here: I search for the bone's node by either name, ID or SID, // and replace the bone's name by the node's name so that the user can use the standard // find-by-name method to associate nodes with bones. - const Collada::Node* bnode = FindNode( pParser.mRootNode, bone->mName.data); - if( !bnode) - bnode = FindNodeBySID( pParser.mRootNode, bone->mName.data); + const Collada::Node* bnode = FindNode(pParser.mRootNode, bone->mName.data); + if (!bnode) + bnode = FindNodeBySID(pParser.mRootNode, bone->mName.data); // assign the name that we would have assigned for the source node - if( bnode) - bone->mName.Set( FindNameForNode( bnode)); + if (bnode) + bone->mName.Set(FindNameForNode(bnode)); else - ASSIMP_LOG_WARN_F( "ColladaLoader::CreateMesh(): could not find corresponding node for joint \"", bone->mName.data, "\"." ); + ASSIMP_LOG_WARN_F("ColladaLoader::CreateMesh(): could not find corresponding node for joint \"", bone->mName.data, "\"."); // and insert bone dstMesh->mBones[boneCount++] = bone; @@ -867,65 +885,65 @@ aiMesh* ColladaLoader::CreateMesh( const ColladaParser& pParser, const Collada:: // ------------------------------------------------------------------------------------------------ // Stores all meshes in the given scene -void ColladaLoader::StoreSceneMeshes( aiScene* pScene) +void ColladaLoader::StoreSceneMeshes(aiScene* pScene) { pScene->mNumMeshes = static_cast(mMeshes.size()); - if( mMeshes.size() > 0) + if (mMeshes.size() > 0) { pScene->mMeshes = new aiMesh*[mMeshes.size()]; - std::copy( mMeshes.begin(), mMeshes.end(), pScene->mMeshes); + std::copy(mMeshes.begin(), mMeshes.end(), pScene->mMeshes); mMeshes.clear(); } } // ------------------------------------------------------------------------------------------------ // Stores all cameras in the given scene -void ColladaLoader::StoreSceneCameras( aiScene* pScene) +void ColladaLoader::StoreSceneCameras(aiScene* pScene) { pScene->mNumCameras = static_cast(mCameras.size()); - if( mCameras.size() > 0) + if (mCameras.size() > 0) { pScene->mCameras = new aiCamera*[mCameras.size()]; - std::copy( mCameras.begin(), mCameras.end(), pScene->mCameras); + std::copy(mCameras.begin(), mCameras.end(), pScene->mCameras); mCameras.clear(); } } // ------------------------------------------------------------------------------------------------ // Stores all lights in the given scene -void ColladaLoader::StoreSceneLights( aiScene* pScene) +void ColladaLoader::StoreSceneLights(aiScene* pScene) { pScene->mNumLights = static_cast(mLights.size()); - if( mLights.size() > 0) + if (mLights.size() > 0) { pScene->mLights = new aiLight*[mLights.size()]; - std::copy( mLights.begin(), mLights.end(), pScene->mLights); + std::copy(mLights.begin(), mLights.end(), pScene->mLights); mLights.clear(); } } // ------------------------------------------------------------------------------------------------ // Stores all textures in the given scene -void ColladaLoader::StoreSceneTextures( aiScene* pScene) +void ColladaLoader::StoreSceneTextures(aiScene* pScene) { pScene->mNumTextures = static_cast(mTextures.size()); - if( mTextures.size() > 0) + if (mTextures.size() > 0) { pScene->mTextures = new aiTexture*[mTextures.size()]; - std::copy( mTextures.begin(), mTextures.end(), pScene->mTextures); + std::copy(mTextures.begin(), mTextures.end(), pScene->mTextures); mTextures.clear(); } } // ------------------------------------------------------------------------------------------------ // Stores all materials in the given scene -void ColladaLoader::StoreSceneMaterials( aiScene* pScene) +void ColladaLoader::StoreSceneMaterials(aiScene* pScene) { pScene->mNumMaterials = static_cast(newMats.size()); if (newMats.size() > 0) { pScene->mMaterials = new aiMaterial*[newMats.size()]; - for (unsigned int i = 0; i < newMats.size();++i) + for (unsigned int i = 0; i < newMats.size(); ++i) pScene->mMaterials[i] = newMats[i].second; newMats.clear(); @@ -934,33 +952,33 @@ void ColladaLoader::StoreSceneMaterials( aiScene* pScene) // ------------------------------------------------------------------------------------------------ // Stores all animations -void ColladaLoader::StoreAnimations( aiScene* pScene, const ColladaParser& pParser) +void ColladaLoader::StoreAnimations(aiScene* pScene, const ColladaParser& pParser) { // recursively collect all animations from the collada scene - StoreAnimations( pScene, pParser, &pParser.mAnims, ""); + StoreAnimations(pScene, pParser, &pParser.mAnims, ""); // catch special case: many animations with the same length, each affecting only a single node. // we need to unite all those single-node-anims to a proper combined animation - for( size_t a = 0; a < mAnims.size(); ++a) + for (size_t a = 0; a < mAnims.size(); ++a) { aiAnimation* templateAnim = mAnims[a]; - if( templateAnim->mNumChannels == 1) + if (templateAnim->mNumChannels == 1) { // search for other single-channel-anims with the same duration std::vector collectedAnimIndices; - for( size_t b = a+1; b < mAnims.size(); ++b) + for (size_t b = a + 1; b < mAnims.size(); ++b) { aiAnimation* other = mAnims[b]; - if( other->mNumChannels == 1 && other->mDuration == templateAnim->mDuration && - other->mTicksPerSecond == templateAnim->mTicksPerSecond ) - collectedAnimIndices.push_back( b); + if (other->mNumChannels == 1 && other->mDuration == templateAnim->mDuration && + other->mTicksPerSecond == templateAnim->mTicksPerSecond) + collectedAnimIndices.push_back(b); } // if there are other animations which fit the template anim, combine all channels into a single anim - if( !collectedAnimIndices.empty() ) + if (!collectedAnimIndices.empty()) { aiAnimation* combinedAnim = new aiAnimation(); - combinedAnim->mName = aiString( std::string( "combinedAnim_") + char( '0' + a)); + combinedAnim->mName = aiString(std::string("combinedAnim_") + char('0' + a)); combinedAnim->mDuration = templateAnim->mDuration; combinedAnim->mTicksPerSecond = templateAnim->mTicksPerSecond; combinedAnim->mNumChannels = static_cast(collectedAnimIndices.size() + 1); @@ -973,7 +991,7 @@ void ColladaLoader::StoreAnimations( aiScene* pScene, const ColladaParser& pPars mAnims[a] = combinedAnim; // move the memory of all other anims to the combined anim and erase them from the source anims - for( size_t b = 0; b < collectedAnimIndices.size(); ++b) + for (size_t b = 0; b < collectedAnimIndices.size(); ++b) { aiAnimation* srcAnimation = mAnims[collectedAnimIndices[b]]; combinedAnim->mChannels[1 + b] = srcAnimation->mChannels[0]; @@ -983,9 +1001,9 @@ void ColladaLoader::StoreAnimations( aiScene* pScene, const ColladaParser& pPars // in a second go, delete all the single-channel-anims that we've stripped from their channels // back to front to preserve indices - you know, removing an element from a vector moves all elements behind the removed one - while( !collectedAnimIndices.empty() ) + while (!collectedAnimIndices.empty()) { - mAnims.erase( mAnims.begin() + collectedAnimIndices.back()); + mAnims.erase(mAnims.begin() + collectedAnimIndices.back()); collectedAnimIndices.pop_back(); } } @@ -993,11 +1011,11 @@ void ColladaLoader::StoreAnimations( aiScene* pScene, const ColladaParser& pPars } // now store all anims in the scene - if( !mAnims.empty()) + if (!mAnims.empty()) { pScene->mNumAnimations = static_cast(mAnims.size()); pScene->mAnimations = new aiAnimation*[mAnims.size()]; - std::copy( mAnims.begin(), mAnims.end(), pScene->mAnimations); + std::copy(mAnims.begin(), mAnims.end(), pScene->mAnimations); } mAnims.clear(); @@ -1005,17 +1023,17 @@ void ColladaLoader::StoreAnimations( aiScene* pScene, const ColladaParser& pPars // ------------------------------------------------------------------------------------------------ // Constructs the animations for the given source anim -void ColladaLoader::StoreAnimations( aiScene* pScene, const ColladaParser& pParser, const Collada::Animation* pSrcAnim, const std::string &pPrefix) +void ColladaLoader::StoreAnimations(aiScene* pScene, const ColladaParser& pParser, const Collada::Animation* pSrcAnim, const std::string &pPrefix) { std::string animName = pPrefix.empty() ? pSrcAnim->mName : pPrefix + "_" + pSrcAnim->mName; // create nested animations, if given - for( std::vector::const_iterator it = pSrcAnim->mSubAnims.begin(); it != pSrcAnim->mSubAnims.end(); ++it) - StoreAnimations( pScene, pParser, *it, animName); + for (std::vector::const_iterator it = pSrcAnim->mSubAnims.begin(); it != pSrcAnim->mSubAnims.end(); ++it) + StoreAnimations(pScene, pParser, *it, animName); // create animation channels, if any - if( !pSrcAnim->mChannels.empty()) - CreateAnimation( pScene, pParser, pSrcAnim, animName); + if (!pSrcAnim->mChannels.empty()) + CreateAnimation(pScene, pParser, pSrcAnim, animName); } struct MorphTimeValues @@ -1056,7 +1074,8 @@ void insertMorphTimeValue(std::vector &values, float time, floa { values[i].mKeys.push_back(k); return; - } else if (time > values[i].mTime && time < values[i+1].mTime) + } + else if (time > values[i].mTime && time < values[i + 1].mTime) { MorphTimeValues val; val.mTime = time; @@ -1082,30 +1101,30 @@ float getWeightAtKey(const std::vector &values, int key, unsign // ------------------------------------------------------------------------------------------------ // Constructs the animation for the given source anim -void ColladaLoader::CreateAnimation( aiScene* pScene, const ColladaParser& pParser, const Collada::Animation* pSrcAnim, const std::string& pName) +void ColladaLoader::CreateAnimation(aiScene* pScene, const ColladaParser& pParser, const Collada::Animation* pSrcAnim, const std::string& pName) { // collect a list of animatable nodes std::vector nodes; - CollectNodes( pScene->mRootNode, nodes); + CollectNodes(pScene->mRootNode, nodes); std::vector anims; std::vector morphAnims; - for( std::vector::const_iterator nit = nodes.begin(); nit != nodes.end(); ++nit) + for (std::vector::const_iterator nit = nodes.begin(); nit != nodes.end(); ++nit) { // find all the collada anim channels which refer to the current node std::vector entries; std::string nodeName = (*nit)->mName.data; // find the collada node corresponding to the aiNode - const Collada::Node* srcNode = FindNode( pParser.mRootNode, nodeName); -// ai_assert( srcNode != NULL); - if( !srcNode) + const Collada::Node* srcNode = FindNode(pParser.mRootNode, nodeName); + // ai_assert( srcNode != NULL); + if (!srcNode) continue; // now check all channels if they affect the current node std::string targetID, subElement; - for( std::vector::const_iterator cit = pSrcAnim->mChannels.begin(); + for (std::vector::const_iterator cit = pSrcAnim->mChannels.begin(); cit != pSrcAnim->mChannels.end(); ++cit) { const Collada::AnimationChannel& srcChannel = *cit; @@ -1113,8 +1132,8 @@ void ColladaLoader::CreateAnimation( aiScene* pScene, const ColladaParser& pPars // we expect the animation target to be of type "nodeName/transformID.subElement". Ignore all others // find the slash that separates the node name - there should be only one - std::string::size_type slashPos = srcChannel.mTarget.find( '/'); - if( slashPos == std::string::npos) + std::string::size_type slashPos = srcChannel.mTarget.find('/'); + if (slashPos == std::string::npos) { std::string::size_type targetPos = srcChannel.mTarget.find(srcNode->mID); if (targetPos == std::string::npos) @@ -1123,44 +1142,45 @@ void ColladaLoader::CreateAnimation( aiScene* pScene, const ColladaParser& pPars // not node transform, but something else. store as unknown animation channel for now entry.mChannel = &(*cit); entry.mTargetId = srcChannel.mTarget.substr(targetPos + pSrcAnim->mName.length(), - srcChannel.mTarget.length() - targetPos - pSrcAnim->mName.length()); + srcChannel.mTarget.length() - targetPos - pSrcAnim->mName.length()); if (entry.mTargetId.front() == '-') entry.mTargetId = entry.mTargetId.substr(1); entries.push_back(entry); continue; } - if( srcChannel.mTarget.find( '/', slashPos+1) != std::string::npos) + if (srcChannel.mTarget.find('/', slashPos + 1) != std::string::npos) continue; targetID.clear(); - targetID = srcChannel.mTarget.substr( 0, slashPos); - if( targetID != srcNode->mID) + targetID = srcChannel.mTarget.substr(0, slashPos); + if (targetID != srcNode->mID) continue; // find the dot that separates the transformID - there should be only one or zero - std::string::size_type dotPos = srcChannel.mTarget.find( '.'); - if( dotPos != std::string::npos) + std::string::size_type dotPos = srcChannel.mTarget.find('.'); + if (dotPos != std::string::npos) { - if( srcChannel.mTarget.find( '.', dotPos+1) != std::string::npos) + if (srcChannel.mTarget.find('.', dotPos + 1) != std::string::npos) continue; - entry.mTransformId = srcChannel.mTarget.substr( slashPos+1, dotPos - slashPos - 1); + entry.mTransformId = srcChannel.mTarget.substr(slashPos + 1, dotPos - slashPos - 1); subElement.clear(); - subElement = srcChannel.mTarget.substr( dotPos+1); - if( subElement == "ANGLE") + subElement = srcChannel.mTarget.substr(dotPos + 1); + if (subElement == "ANGLE") entry.mSubElement = 3; // last number in an Axis-Angle-Transform is the angle - else if( subElement == "X") + else if (subElement == "X") entry.mSubElement = 0; - else if( subElement == "Y") + else if (subElement == "Y") entry.mSubElement = 1; - else if( subElement == "Z") + else if (subElement == "Z") entry.mSubElement = 2; else - ASSIMP_LOG_WARN_F( "Unknown anim subelement <", subElement, ">. Ignoring" ); - } else { + ASSIMP_LOG_WARN_F("Unknown anim subelement <", subElement, ">. Ignoring"); + } + else { // no subelement following, transformId is remaining string - entry.mTransformId = srcChannel.mTarget.substr( slashPos+1); + entry.mTransformId = srcChannel.mTarget.substr(slashPos + 1); } std::string::size_type bracketPos = srcChannel.mTarget.find('('); @@ -1206,194 +1226,196 @@ void ColladaLoader::CreateAnimation( aiScene* pScene, const ColladaParser& pPars // determine which transform step is affected by this channel entry.mTransformIndex = SIZE_MAX; - for( size_t a = 0; a < srcNode->mTransforms.size(); ++a) - if( srcNode->mTransforms[a].mID == entry.mTransformId) + for (size_t a = 0; a < srcNode->mTransforms.size(); ++a) + if (srcNode->mTransforms[a].mID == entry.mTransformId) entry.mTransformIndex = a; - if( entry.mTransformIndex == SIZE_MAX) + if (entry.mTransformIndex == SIZE_MAX) { if (entry.mTransformId.find("morph-weights") != std::string::npos) { entry.mTargetId = entry.mTransformId; entry.mTransformId = ""; - } else + } + else continue; } entry.mChannel = &(*cit); - entries.push_back( entry); + entries.push_back(entry); } // if there's no channel affecting the current node, we skip it - if( entries.empty()) + if (entries.empty()) continue; // resolve the data pointers for all anim channels. Find the minimum time while we're at it - ai_real startTime = ai_real( 1e20 ), endTime = ai_real( -1e20 ); - for( std::vector::iterator it = entries.begin(); it != entries.end(); ++it) + ai_real startTime = ai_real(1e20), endTime = ai_real(-1e20); + for (std::vector::iterator it = entries.begin(); it != entries.end(); ++it) { Collada::ChannelEntry& e = *it; - e.mTimeAccessor = &pParser.ResolveLibraryReference( pParser.mAccessorLibrary, e.mChannel->mSourceTimes); - e.mTimeData = &pParser.ResolveLibraryReference( pParser.mDataLibrary, e.mTimeAccessor->mSource); - e.mValueAccessor = &pParser.ResolveLibraryReference( pParser.mAccessorLibrary, e.mChannel->mSourceValues); - e.mValueData = &pParser.ResolveLibraryReference( pParser.mDataLibrary, e.mValueAccessor->mSource); + e.mTimeAccessor = &pParser.ResolveLibraryReference(pParser.mAccessorLibrary, e.mChannel->mSourceTimes); + e.mTimeData = &pParser.ResolveLibraryReference(pParser.mDataLibrary, e.mTimeAccessor->mSource); + e.mValueAccessor = &pParser.ResolveLibraryReference(pParser.mAccessorLibrary, e.mChannel->mSourceValues); + e.mValueData = &pParser.ResolveLibraryReference(pParser.mDataLibrary, e.mValueAccessor->mSource); // time count and value count must match - if( e.mTimeAccessor->mCount != e.mValueAccessor->mCount) - throw DeadlyImportError( format() << "Time count / value count mismatch in animation channel \"" << e.mChannel->mTarget << "\"." ); + if (e.mTimeAccessor->mCount != e.mValueAccessor->mCount) + throw DeadlyImportError(format() << "Time count / value count mismatch in animation channel \"" << e.mChannel->mTarget << "\"."); - if( e.mTimeAccessor->mCount > 0 ) - { - // find bounding times - startTime = std::min( startTime, ReadFloat( *e.mTimeAccessor, *e.mTimeData, 0, 0)); - endTime = std::max( endTime, ReadFloat( *e.mTimeAccessor, *e.mTimeData, e.mTimeAccessor->mCount-1, 0)); - } + if (e.mTimeAccessor->mCount > 0) + { + // find bounding times + startTime = std::min(startTime, ReadFloat(*e.mTimeAccessor, *e.mTimeData, 0, 0)); + endTime = std::max(endTime, ReadFloat(*e.mTimeAccessor, *e.mTimeData, e.mTimeAccessor->mCount - 1, 0)); + } } - std::vector resultTrafos; - if( !entries.empty() && entries.front().mTimeAccessor->mCount > 0 ) - { - // create a local transformation chain of the node's transforms - std::vector transforms = srcNode->mTransforms; + std::vector resultTrafos; + if (!entries.empty() && entries.front().mTimeAccessor->mCount > 0) + { + // create a local transformation chain of the node's transforms + std::vector transforms = srcNode->mTransforms; - // now for every unique point in time, find or interpolate the key values for that time - // and apply them to the transform chain. Then the node's present transformation can be calculated. - ai_real time = startTime; - while( 1) - { - for( std::vector::iterator it = entries.begin(); it != entries.end(); ++it) - { - Collada::ChannelEntry& e = *it; + // now for every unique point in time, find or interpolate the key values for that time + // and apply them to the transform chain. Then the node's present transformation can be calculated. + ai_real time = startTime; + while (1) + { + for (std::vector::iterator it = entries.begin(); it != entries.end(); ++it) + { + Collada::ChannelEntry& e = *it; - // find the keyframe behind the current point in time - size_t pos = 0; - ai_real postTime = 0.0; - while( 1) - { - if( pos >= e.mTimeAccessor->mCount) - break; - postTime = ReadFloat( *e.mTimeAccessor, *e.mTimeData, pos, 0); - if( postTime >= time) - break; - ++pos; - } + // find the keyframe behind the current point in time + size_t pos = 0; + ai_real postTime = 0.0; + while (1) + { + if (pos >= e.mTimeAccessor->mCount) + break; + postTime = ReadFloat(*e.mTimeAccessor, *e.mTimeData, pos, 0); + if (postTime >= time) + break; + ++pos; + } - pos = std::min( pos, e.mTimeAccessor->mCount-1); + pos = std::min(pos, e.mTimeAccessor->mCount - 1); - // read values from there - ai_real temp[16]; - for( size_t c = 0; c < e.mValueAccessor->mSize; ++c) - temp[c] = ReadFloat( *e.mValueAccessor, *e.mValueData, pos, c); + // read values from there + ai_real temp[16]; + for (size_t c = 0; c < e.mValueAccessor->mSize; ++c) + temp[c] = ReadFloat(*e.mValueAccessor, *e.mValueData, pos, c); - // if not exactly at the key time, interpolate with previous value set - if( postTime > time && pos > 0) - { - ai_real preTime = ReadFloat( *e.mTimeAccessor, *e.mTimeData, pos-1, 0); - ai_real factor = (time - postTime) / (preTime - postTime); + // if not exactly at the key time, interpolate with previous value set + if (postTime > time && pos > 0) + { + ai_real preTime = ReadFloat(*e.mTimeAccessor, *e.mTimeData, pos - 1, 0); + ai_real factor = (time - postTime) / (preTime - postTime); - for( size_t c = 0; c < e.mValueAccessor->mSize; ++c) - { - ai_real v = ReadFloat( *e.mValueAccessor, *e.mValueData, pos-1, c); - temp[c] += (v - temp[c]) * factor; - } - } + for (size_t c = 0; c < e.mValueAccessor->mSize; ++c) + { + ai_real v = ReadFloat(*e.mValueAccessor, *e.mValueData, pos - 1, c); + temp[c] += (v - temp[c]) * factor; + } + } - // Apply values to current transformation - std::copy( temp, temp + e.mValueAccessor->mSize, transforms[e.mTransformIndex].f + e.mSubElement); - } + // Apply values to current transformation + std::copy(temp, temp + e.mValueAccessor->mSize, transforms[e.mTransformIndex].f + e.mSubElement); + } - // Calculate resulting transformation - aiMatrix4x4 mat = pParser.CalculateResultTransform( transforms); + // Calculate resulting transformation + aiMatrix4x4 mat = pParser.CalculateResultTransform(transforms); - // out of laziness: we store the time in matrix.d4 - mat.d4 = time; - resultTrafos.push_back( mat); + // out of laziness: we store the time in matrix.d4 + mat.d4 = time; + resultTrafos.push_back(mat); - // find next point in time to evaluate. That's the closest frame larger than the current in any channel - ai_real nextTime = ai_real( 1e20 ); - for( std::vector::iterator it = entries.begin(); it != entries.end(); ++it) - { - Collada::ChannelEntry& channelElement = *it; + // find next point in time to evaluate. That's the closest frame larger than the current in any channel + ai_real nextTime = ai_real(1e20); + for (std::vector::iterator it = entries.begin(); it != entries.end(); ++it) + { + Collada::ChannelEntry& channelElement = *it; - // find the next time value larger than the current - size_t pos = 0; - while( pos < channelElement.mTimeAccessor->mCount) - { - const ai_real t = ReadFloat( *channelElement.mTimeAccessor, *channelElement.mTimeData, pos, 0); - if( t > time) - { - nextTime = std::min( nextTime, t); - break; - } - ++pos; - } + // find the next time value larger than the current + size_t pos = 0; + while (pos < channelElement.mTimeAccessor->mCount) + { + const ai_real t = ReadFloat(*channelElement.mTimeAccessor, *channelElement.mTimeData, pos, 0); + if (t > time) + { + nextTime = std::min(nextTime, t); + break; + } + ++pos; + } - // https://github.com/assimp/assimp/issues/458 - // Sub-sample axis-angle channels if the delta between two consecutive - // key-frame angles is >= 180 degrees. - if (transforms[channelElement.mTransformIndex].mType == Collada::TF_ROTATE && channelElement.mSubElement == 3 && pos > 0 && pos < channelElement.mTimeAccessor->mCount) { - const ai_real cur_key_angle = ReadFloat(*channelElement.mValueAccessor, *channelElement.mValueData, pos, 0); - const ai_real last_key_angle = ReadFloat(*channelElement.mValueAccessor, *channelElement.mValueData, pos - 1, 0); - const ai_real cur_key_time = ReadFloat(*channelElement.mTimeAccessor, *channelElement.mTimeData, pos, 0); - const ai_real last_key_time = ReadFloat(*channelElement.mTimeAccessor, *channelElement.mTimeData, pos - 1, 0); - const ai_real last_eval_angle = last_key_angle + (cur_key_angle - last_key_angle) * (time - last_key_time) / (cur_key_time - last_key_time); - const ai_real delta = std::abs(cur_key_angle - last_eval_angle); - if (delta >= 180.0) { - const int subSampleCount = static_cast(std::floor(delta / 90.0)); - if (cur_key_time != time) { - const ai_real nextSampleTime = time + (cur_key_time - time) / subSampleCount; - nextTime = std::min(nextTime, nextSampleTime); - } - } - } - } + // https://github.com/assimp/assimp/issues/458 + // Sub-sample axis-angle channels if the delta between two consecutive + // key-frame angles is >= 180 degrees. + if (transforms[channelElement.mTransformIndex].mType == Collada::TF_ROTATE && channelElement.mSubElement == 3 && pos > 0 && pos < channelElement.mTimeAccessor->mCount) { + const ai_real cur_key_angle = ReadFloat(*channelElement.mValueAccessor, *channelElement.mValueData, pos, 0); + const ai_real last_key_angle = ReadFloat(*channelElement.mValueAccessor, *channelElement.mValueData, pos - 1, 0); + const ai_real cur_key_time = ReadFloat(*channelElement.mTimeAccessor, *channelElement.mTimeData, pos, 0); + const ai_real last_key_time = ReadFloat(*channelElement.mTimeAccessor, *channelElement.mTimeData, pos - 1, 0); + const ai_real last_eval_angle = last_key_angle + (cur_key_angle - last_key_angle) * (time - last_key_time) / (cur_key_time - last_key_time); + const ai_real delta = std::abs(cur_key_angle - last_eval_angle); + if (delta >= 180.0) { + const int subSampleCount = static_cast(std::floor(delta / 90.0)); + if (cur_key_time != time) { + const ai_real nextSampleTime = time + (cur_key_time - time) / subSampleCount; + nextTime = std::min(nextTime, nextSampleTime); + } + } + } + } - // no more keys on any channel after the current time -> we're done - if( nextTime > 1e19) - break; + // no more keys on any channel after the current time -> we're done + if (nextTime > 1e19) + break; - // else construct next keyframe at this following time point - time = nextTime; - } - } + // else construct next keyframe at this following time point + time = nextTime; + } + } // there should be some keyframes, but we aren't that fixated on valid input data // ai_assert( resultTrafos.size() > 0); // build an animation channel for the given node out of these trafo keys - if( !resultTrafos.empty() ) + if (!resultTrafos.empty()) { - aiNodeAnim* dstAnim = new aiNodeAnim; - dstAnim->mNodeName = nodeName; - dstAnim->mNumPositionKeys = static_cast(resultTrafos.size()); - dstAnim->mNumRotationKeys = static_cast(resultTrafos.size()); - dstAnim->mNumScalingKeys = static_cast(resultTrafos.size()); - dstAnim->mPositionKeys = new aiVectorKey[resultTrafos.size()]; - dstAnim->mRotationKeys = new aiQuatKey[resultTrafos.size()]; - dstAnim->mScalingKeys = new aiVectorKey[resultTrafos.size()]; + aiNodeAnim* dstAnim = new aiNodeAnim; + dstAnim->mNodeName = nodeName; + dstAnim->mNumPositionKeys = static_cast(resultTrafos.size()); + dstAnim->mNumRotationKeys = static_cast(resultTrafos.size()); + dstAnim->mNumScalingKeys = static_cast(resultTrafos.size()); + dstAnim->mPositionKeys = new aiVectorKey[resultTrafos.size()]; + dstAnim->mRotationKeys = new aiQuatKey[resultTrafos.size()]; + dstAnim->mScalingKeys = new aiVectorKey[resultTrafos.size()]; - for( size_t a = 0; a < resultTrafos.size(); ++a) - { - aiMatrix4x4 mat = resultTrafos[a]; - double time = double( mat.d4); // remember? time is stored in mat.d4 - mat.d4 = 1.0f; + for (size_t a = 0; a < resultTrafos.size(); ++a) + { + aiMatrix4x4 mat = resultTrafos[a]; + double time = double(mat.d4); // remember? time is stored in mat.d4 + mat.d4 = 1.0f; - dstAnim->mPositionKeys[a].mTime = time; - dstAnim->mRotationKeys[a].mTime = time; - dstAnim->mScalingKeys[a].mTime = time; - mat.Decompose( dstAnim->mScalingKeys[a].mValue, dstAnim->mRotationKeys[a].mValue, dstAnim->mPositionKeys[a].mValue); - } + dstAnim->mPositionKeys[a].mTime = time; + dstAnim->mRotationKeys[a].mTime = time; + dstAnim->mScalingKeys[a].mTime = time; + mat.Decompose(dstAnim->mScalingKeys[a].mValue, dstAnim->mRotationKeys[a].mValue, dstAnim->mPositionKeys[a].mValue); + } - anims.push_back( dstAnim); - } else + anims.push_back(dstAnim); + } + else { - ASSIMP_LOG_WARN( "Collada loader: found empty animation channel, ignored. Please check your exporter."); + ASSIMP_LOG_WARN("Collada loader: found empty animation channel, ignored. Please check your exporter."); } - if( !entries.empty() && entries.front().mTimeAccessor->mCount > 0 ) + if (!entries.empty() && entries.front().mTimeAccessor->mCount > 0) { std::vector morphChannels; - for( std::vector::iterator it = entries.begin(); it != entries.end(); ++it) + for (std::vector::iterator it = entries.begin(); it != entries.end(); ++it) { Collada::ChannelEntry& e = *it; @@ -1416,7 +1438,7 @@ void ColladaLoader::CreateAnimation( aiScene* pScene, const ColladaParser& pPars std::vector morphTimeValues; int morphAnimChannelIndex = 0; - for( std::vector::iterator it = morphChannels.begin(); it != morphChannels.end(); ++it) + for (std::vector::iterator it = morphChannels.begin(); it != morphChannels.end(); ++it) { Collada::ChannelEntry& e = *it; std::string::size_type apos = e.mTargetId.find('('); @@ -1438,8 +1460,8 @@ void ColladaLoader::CreateAnimation( aiScene* pScene, const ColladaParser& pPars for (unsigned int key = 0; key < morphAnim->mNumKeys; key++) { morphAnim->mKeys[key].mNumValuesAndWeights = static_cast(morphChannels.size()); - morphAnim->mKeys[key].mValues = new unsigned int [morphChannels.size()]; - morphAnim->mKeys[key].mWeights = new double [morphChannels.size()]; + morphAnim->mKeys[key].mValues = new unsigned int[morphChannels.size()]; + morphAnim->mKeys[key].mWeights = new double[morphChannels.size()]; morphAnim->mKeys[key].mTime = morphTimeValues[key].mTime; for (unsigned int valueIndex = 0; valueIndex < morphChannels.size(); valueIndex++) @@ -1454,48 +1476,48 @@ void ColladaLoader::CreateAnimation( aiScene* pScene, const ColladaParser& pPars } } - if( !anims.empty() || !morphAnims.empty()) + if (!anims.empty() || !morphAnims.empty()) { aiAnimation* anim = new aiAnimation; - anim->mName.Set( pName); + anim->mName.Set(pName); anim->mNumChannels = static_cast(anims.size()); if (anim->mNumChannels > 0) { anim->mChannels = new aiNodeAnim*[anims.size()]; - std::copy( anims.begin(), anims.end(), anim->mChannels); + std::copy(anims.begin(), anims.end(), anim->mChannels); } anim->mNumMorphMeshChannels = static_cast(morphAnims.size()); if (anim->mNumMorphMeshChannels > 0) { anim->mMorphMeshChannels = new aiMeshMorphAnim*[anim->mNumMorphMeshChannels]; - std::copy( morphAnims.begin(), morphAnims.end(), anim->mMorphMeshChannels); + std::copy(morphAnims.begin(), morphAnims.end(), anim->mMorphMeshChannels); } anim->mDuration = 0.0f; - for( size_t a = 0; a < anims.size(); ++a) + for (size_t a = 0; a < anims.size(); ++a) { - anim->mDuration = std::max( anim->mDuration, anims[a]->mPositionKeys[anims[a]->mNumPositionKeys-1].mTime); - anim->mDuration = std::max( anim->mDuration, anims[a]->mRotationKeys[anims[a]->mNumRotationKeys-1].mTime); - anim->mDuration = std::max( anim->mDuration, anims[a]->mScalingKeys[anims[a]->mNumScalingKeys-1].mTime); + anim->mDuration = std::max(anim->mDuration, anims[a]->mPositionKeys[anims[a]->mNumPositionKeys - 1].mTime); + anim->mDuration = std::max(anim->mDuration, anims[a]->mRotationKeys[anims[a]->mNumRotationKeys - 1].mTime); + anim->mDuration = std::max(anim->mDuration, anims[a]->mScalingKeys[anims[a]->mNumScalingKeys - 1].mTime); } for (size_t a = 0; a < morphAnims.size(); ++a) { - anim->mDuration = std::max(anim->mDuration, morphAnims[a]->mKeys[morphAnims[a]->mNumKeys-1].mTime); + anim->mDuration = std::max(anim->mDuration, morphAnims[a]->mKeys[morphAnims[a]->mNumKeys - 1].mTime); } anim->mTicksPerSecond = 1; - mAnims.push_back( anim); + mAnims.push_back(anim); } } // ------------------------------------------------------------------------------------------------ // Add a texture to a material structure -void ColladaLoader::AddTexture ( aiMaterial& mat, const ColladaParser& pParser, +void ColladaLoader::AddTexture(aiMaterial& mat, const ColladaParser& pParser, const Collada::Effect& effect, const Collada::Sampler& sampler, aiTextureType type, unsigned int idx) { // first of all, basic file name - const aiString name = FindFilenameForEffectTexture( pParser, effect, sampler.mName ); - mat.AddProperty( &name, _AI_MATKEY_TEXTURE_BASE, type, idx ); + const aiString name = FindFilenameForEffectTexture(pParser, effect, sampler.mName); + mat.AddProperty(&name, _AI_MATKEY_TEXTURE_BASE, type, idx); // mapping mode int map = aiTextureMapMode_Clamp; @@ -1504,7 +1526,7 @@ void ColladaLoader::AddTexture ( aiMaterial& mat, const ColladaParser& pParser, if (sampler.mWrapU && sampler.mMirrorU) map = aiTextureMapMode_Mirror; - mat.AddProperty( &map, 1, _AI_MATKEY_MAPPINGMODE_U_BASE, type, idx); + mat.AddProperty(&map, 1, _AI_MATKEY_MAPPINGMODE_U_BASE, type, idx); map = aiTextureMapMode_Clamp; if (sampler.mWrapV) @@ -1512,18 +1534,18 @@ void ColladaLoader::AddTexture ( aiMaterial& mat, const ColladaParser& pParser, if (sampler.mWrapV && sampler.mMirrorV) map = aiTextureMapMode_Mirror; - mat.AddProperty( &map, 1, _AI_MATKEY_MAPPINGMODE_V_BASE, type, idx); + mat.AddProperty(&map, 1, _AI_MATKEY_MAPPINGMODE_V_BASE, type, idx); // UV transformation mat.AddProperty(&sampler.mTransform, 1, _AI_MATKEY_UVTRANSFORM_BASE, type, idx); // Blend mode - mat.AddProperty((int*)&sampler.mOp , 1, + mat.AddProperty((int*)&sampler.mOp, 1, _AI_MATKEY_TEXBLEND_BASE, type, idx); // Blend factor - mat.AddProperty((ai_real*)&sampler.mWeighting , 1, + mat.AddProperty((ai_real*)&sampler.mWeighting, 1, _AI_MATKEY_TEXBLEND_BASE, type, idx); // UV source index ... if we didn't resolve the mapping, it is actually just @@ -1535,7 +1557,7 @@ void ColladaLoader::AddTexture ( aiMaterial& mat, const ColladaParser& pParser, map = sampler.mUVId; else { map = -1; - for (std::string::const_iterator it = sampler.mUVChannel.begin();it != sampler.mUVChannel.end(); ++it){ + for (std::string::const_iterator it = sampler.mUVChannel.begin(); it != sampler.mUVChannel.end(); ++it) { if (IsNumeric(*it)) { map = strtoul10(&(*it)); break; @@ -1546,12 +1568,12 @@ void ColladaLoader::AddTexture ( aiMaterial& mat, const ColladaParser& pParser, map = 0; } } - mat.AddProperty(&map,1,_AI_MATKEY_UVWSRC_BASE,type,idx); + mat.AddProperty(&map, 1, _AI_MATKEY_UVWSRC_BASE, type, idx); } // ------------------------------------------------------------------------------------------------ // Fills materials from the collada material definitions -void ColladaLoader::FillMaterials( const ColladaParser& pParser, aiScene* /*pScene*/) +void ColladaLoader::FillMaterials(const ColladaParser& pParser, aiScene* /*pScene*/) { for (auto &elem : newMats) { @@ -1563,7 +1585,7 @@ void ColladaLoader::FillMaterials( const ColladaParser& pParser, aiScene* /*pSce if (effect.mFaceted) /* fixme */ shadeMode = aiShadingMode_Flat; else { - switch( effect.mShadeType) + switch (effect.mShadeType) { case Collada::Shade_Constant: shadeMode = aiShadingMode_NoShading; @@ -1584,56 +1606,57 @@ void ColladaLoader::FillMaterials( const ColladaParser& pParser, aiScene* /*pSce break; } } - mat.AddProperty( &shadeMode, 1, AI_MATKEY_SHADING_MODEL); + mat.AddProperty(&shadeMode, 1, AI_MATKEY_SHADING_MODEL); // double-sided? shadeMode = effect.mDoubleSided; - mat.AddProperty( &shadeMode, 1, AI_MATKEY_TWOSIDED); + mat.AddProperty(&shadeMode, 1, AI_MATKEY_TWOSIDED); // wireframe? shadeMode = effect.mWireframe; - mat.AddProperty( &shadeMode, 1, AI_MATKEY_ENABLE_WIREFRAME); + mat.AddProperty(&shadeMode, 1, AI_MATKEY_ENABLE_WIREFRAME); // add material colors - mat.AddProperty( &effect.mAmbient, 1,AI_MATKEY_COLOR_AMBIENT); - mat.AddProperty( &effect.mDiffuse, 1, AI_MATKEY_COLOR_DIFFUSE); - mat.AddProperty( &effect.mSpecular, 1,AI_MATKEY_COLOR_SPECULAR); - mat.AddProperty( &effect.mEmissive, 1, AI_MATKEY_COLOR_EMISSIVE); - mat.AddProperty( &effect.mReflective, 1, AI_MATKEY_COLOR_REFLECTIVE); + mat.AddProperty(&effect.mAmbient, 1, AI_MATKEY_COLOR_AMBIENT); + mat.AddProperty(&effect.mDiffuse, 1, AI_MATKEY_COLOR_DIFFUSE); + mat.AddProperty(&effect.mSpecular, 1, AI_MATKEY_COLOR_SPECULAR); + mat.AddProperty(&effect.mEmissive, 1, AI_MATKEY_COLOR_EMISSIVE); + mat.AddProperty(&effect.mReflective, 1, AI_MATKEY_COLOR_REFLECTIVE); // scalar properties - mat.AddProperty( &effect.mShininess, 1, AI_MATKEY_SHININESS); - mat.AddProperty( &effect.mReflectivity, 1, AI_MATKEY_REFLECTIVITY); - mat.AddProperty( &effect.mRefractIndex, 1, AI_MATKEY_REFRACTI); + mat.AddProperty(&effect.mShininess, 1, AI_MATKEY_SHININESS); + mat.AddProperty(&effect.mReflectivity, 1, AI_MATKEY_REFLECTIVITY); + mat.AddProperty(&effect.mRefractIndex, 1, AI_MATKEY_REFRACTI); // transparency, a very hard one. seemingly not all files are following the // specification here (1.0 transparency => completely opaque)... // therefore, we let the opportunity for the user to manually invert // the transparency if necessary and we add preliminary support for RGB_ZERO mode - if(effect.mTransparency >= 0.f && effect.mTransparency <= 1.f) { + if (effect.mTransparency >= 0.f && effect.mTransparency <= 1.f) { // handle RGB transparency completely, cf Collada specs 1.5.0 pages 249 and 304 - if(effect.mRGBTransparency) { - // use luminance as defined by ISO/CIE color standards (see ITU-R Recommendation BT.709-4) + if (effect.mRGBTransparency) { + // use luminance as defined by ISO/CIE color standards (see ITU-R Recommendation BT.709-4) effect.mTransparency *= ( 0.212671f * effect.mTransparent.r + 0.715160f * effect.mTransparent.g + 0.072169f * effect.mTransparent.b - ); + ); effect.mTransparent.a = 1.f; - mat.AddProperty( &effect.mTransparent, 1, AI_MATKEY_COLOR_TRANSPARENT ); - } else { - effect.mTransparency *= effect.mTransparent.a; + mat.AddProperty(&effect.mTransparent, 1, AI_MATKEY_COLOR_TRANSPARENT); + } + else { + effect.mTransparency *= effect.mTransparent.a; } - if(effect.mInvertTransparency) { + if (effect.mInvertTransparency) { effect.mTransparency = 1.f - effect.mTransparency; } // Is the material finally transparent ? if (effect.mHasTransparency || effect.mTransparency < 1.f) { - mat.AddProperty( &effect.mTransparency, 1, AI_MATKEY_OPACITY ); + mat.AddProperty(&effect.mTransparency, 1, AI_MATKEY_OPACITY); } } @@ -1643,87 +1666,87 @@ void ColladaLoader::FillMaterials( const ColladaParser& pParser, aiScene* /*pSce AddTexture(mat, pParser, effect, effect.mTexAmbient, aiTextureType_LIGHTMAP); } - if( !effect.mTexEmissive.mName.empty()) - AddTexture( mat, pParser, effect, effect.mTexEmissive, aiTextureType_EMISSIVE); + if (!effect.mTexEmissive.mName.empty()) + AddTexture(mat, pParser, effect, effect.mTexEmissive, aiTextureType_EMISSIVE); - if( !effect.mTexSpecular.mName.empty()) - AddTexture( mat, pParser, effect, effect.mTexSpecular, aiTextureType_SPECULAR); + if (!effect.mTexSpecular.mName.empty()) + AddTexture(mat, pParser, effect, effect.mTexSpecular, aiTextureType_SPECULAR); - if( !effect.mTexDiffuse.mName.empty()) - AddTexture( mat, pParser, effect, effect.mTexDiffuse, aiTextureType_DIFFUSE); + if (!effect.mTexDiffuse.mName.empty()) + AddTexture(mat, pParser, effect, effect.mTexDiffuse, aiTextureType_DIFFUSE); - if( !effect.mTexBump.mName.empty()) - AddTexture( mat, pParser, effect, effect.mTexBump, aiTextureType_NORMALS); + if (!effect.mTexBump.mName.empty()) + AddTexture(mat, pParser, effect, effect.mTexBump, aiTextureType_NORMALS); - if( !effect.mTexTransparent.mName.empty()) - AddTexture( mat, pParser, effect, effect.mTexTransparent, aiTextureType_OPACITY); + if (!effect.mTexTransparent.mName.empty()) + AddTexture(mat, pParser, effect, effect.mTexTransparent, aiTextureType_OPACITY); - if( !effect.mTexReflective.mName.empty()) - AddTexture( mat, pParser, effect, effect.mTexReflective, aiTextureType_REFLECTION); + if (!effect.mTexReflective.mName.empty()) + AddTexture(mat, pParser, effect, effect.mTexReflective, aiTextureType_REFLECTION); } } // ------------------------------------------------------------------------------------------------ // Constructs materials from the collada material definitions -void ColladaLoader::BuildMaterials( ColladaParser& pParser, aiScene* /*pScene*/) +void ColladaLoader::BuildMaterials(ColladaParser& pParser, aiScene* /*pScene*/) { newMats.reserve(pParser.mMaterialLibrary.size()); - for( ColladaParser::MaterialLibrary::const_iterator matIt = pParser.mMaterialLibrary.begin(); + for (ColladaParser::MaterialLibrary::const_iterator matIt = pParser.mMaterialLibrary.begin(); matIt != pParser.mMaterialLibrary.end(); ++matIt) { const Collada::Material& material = matIt->second; // a material is only a reference to an effect - ColladaParser::EffectLibrary::iterator effIt = pParser.mEffectLibrary.find( material.mEffect); - if( effIt == pParser.mEffectLibrary.end()) + ColladaParser::EffectLibrary::iterator effIt = pParser.mEffectLibrary.find(material.mEffect); + if (effIt == pParser.mEffectLibrary.end()) continue; Collada::Effect& effect = effIt->second; // create material aiMaterial* mat = new aiMaterial; - aiString name( material.mName.empty() ? matIt->first : material.mName ); - mat->AddProperty(&name,AI_MATKEY_NAME); + aiString name(material.mName.empty() ? matIt->first : material.mName); + mat->AddProperty(&name, AI_MATKEY_NAME); // store the material mMaterialIndexByName[matIt->first] = newMats.size(); - newMats.push_back( std::pair( &effect,mat) ); + newMats.push_back(std::pair(&effect, mat)); } // ScenePreprocessor generates a default material automatically if none is there. // All further code here in this loader works well without a valid material so // we can safely let it to ScenePreprocessor. #if 0 - if( newMats.size() == 0) + if (newMats.size() == 0) { aiMaterial* mat = new aiMaterial; - aiString name( AI_DEFAULT_MATERIAL_NAME ); - mat->AddProperty( &name, AI_MATKEY_NAME); + aiString name(AI_DEFAULT_MATERIAL_NAME); + mat->AddProperty(&name, AI_MATKEY_NAME); const int shadeMode = aiShadingMode_Phong; - mat->AddProperty( &shadeMode, 1, AI_MATKEY_SHADING_MODEL); - aiColor4D colAmbient( 0.2, 0.2, 0.2, 1.0), colDiffuse( 0.8, 0.8, 0.8, 1.0), colSpecular( 0.5, 0.5, 0.5, 0.5); - mat->AddProperty( &colAmbient, 1, AI_MATKEY_COLOR_AMBIENT); - mat->AddProperty( &colDiffuse, 1, AI_MATKEY_COLOR_DIFFUSE); - mat->AddProperty( &colSpecular, 1, AI_MATKEY_COLOR_SPECULAR); + mat->AddProperty(&shadeMode, 1, AI_MATKEY_SHADING_MODEL); + aiColor4D colAmbient(0.2, 0.2, 0.2, 1.0), colDiffuse(0.8, 0.8, 0.8, 1.0), colSpecular(0.5, 0.5, 0.5, 0.5); + mat->AddProperty(&colAmbient, 1, AI_MATKEY_COLOR_AMBIENT); + mat->AddProperty(&colDiffuse, 1, AI_MATKEY_COLOR_DIFFUSE); + mat->AddProperty(&colSpecular, 1, AI_MATKEY_COLOR_SPECULAR); const ai_real specExp = 5.0; - mat->AddProperty( &specExp, 1, AI_MATKEY_SHININESS); + mat->AddProperty(&specExp, 1, AI_MATKEY_SHININESS); } #endif } // ------------------------------------------------------------------------------------------------ // Resolves the texture name for the given effect texture entry -aiString ColladaLoader::FindFilenameForEffectTexture( const ColladaParser& pParser, +aiString ColladaLoader::FindFilenameForEffectTexture(const ColladaParser& pParser, const Collada::Effect& pEffect, const std::string& pName) { aiString result; // recurse through the param references until we end up at an image std::string name = pName; - while( 1) + while (1) { // the given string is a param entry. Find it - Collada::Effect::ParamLibrary::const_iterator it = pEffect.mParams.find( name); + Collada::Effect::ParamLibrary::const_iterator it = pEffect.mParams.find(name); // if not found, we're at the end of the recursion. The resulting string should be the image ID - if( it == pEffect.mParams.end()) + if (it == pEffect.mParams.end()) break; // else recurse on @@ -1731,8 +1754,8 @@ aiString ColladaLoader::FindFilenameForEffectTexture( const ColladaParser& pPars } // find the image referred by this name in the image library of the scene - ColladaParser::ImageLibrary::const_iterator imIt = pParser.mImageLibrary.find( name); - if( imIt == pParser.mImageLibrary.end()) + ColladaParser::ImageLibrary::const_iterator imIt = pParser.mImageLibrary.find(name); + if (imIt == pParser.mImageLibrary.end()) { ASSIMP_LOG_WARN_F("Collada: Unable to resolve effect texture entry \"", pName, "\", ended up at ID \"", name, "\"."); @@ -1745,7 +1768,7 @@ aiString ColladaLoader::FindFilenameForEffectTexture( const ColladaParser& pPars // if this is an embedded texture image setup an aiTexture for it if (imIt->second.mFileName.empty()) { - if (imIt->second.mImageData.empty()) { + if (imIt->second.mImageData.empty()) { throw DeadlyImportError("Collada: Invalid texture, no data or file reference given"); } @@ -1755,13 +1778,13 @@ aiString ColladaLoader::FindFilenameForEffectTexture( const ColladaParser& pPars if (imIt->second.mEmbeddedFormat.length() > 3) { ASSIMP_LOG_WARN("Collada: texture format hint is too long, truncating to 3 characters"); } - strncpy(tex->achFormatHint,imIt->second.mEmbeddedFormat.c_str(),3); + strncpy(tex->achFormatHint, imIt->second.mEmbeddedFormat.c_str(), 3); // and copy texture data tex->mHeight = 0; tex->mWidth = static_cast(imIt->second.mImageData.size()); tex->pcData = (aiTexel*)new char[tex->mWidth]; - memcpy(tex->pcData,&imIt->second.mImageData[0],tex->mWidth); + memcpy(tex->pcData, &imIt->second.mImageData[0], tex->mWidth); // TODO: check the possibility of using the flag "AI_CONFIG_IMPORT_FBX_EMBEDDED_TEXTURES_LEGACY_NAMING" // In FBX files textures are now stored internally by Assimp with their filename included @@ -1770,14 +1793,14 @@ aiString ColladaLoader::FindFilenameForEffectTexture( const ColladaParser& pPars // This may occur on this case too, it has to be studied // setup texture reference string result.data[0] = '*'; - result.length = 1 + ASSIMP_itoa10(result.data+1,static_cast(MAXLEN-1),static_cast(mTextures.size())); + result.length = 1 + ASSIMP_itoa10(result.data + 1, static_cast(MAXLEN - 1), static_cast(mTextures.size())); // and add this texture to the list mTextures.push_back(tex); } else { - result.Set( imIt->second.mFileName ); + result.Set(imIt->second.mFileName); ConvertPath(result); } return result; @@ -1785,77 +1808,78 @@ aiString ColladaLoader::FindFilenameForEffectTexture( const ColladaParser& pPars // ------------------------------------------------------------------------------------------------ // Convert a path read from a collada file to the usual representation -void ColladaLoader::ConvertPath (aiString& ss) +void ColladaLoader::ConvertPath(aiString& ss) { // TODO: collada spec, p 22. Handle URI correctly. // For the moment we're just stripping the file:// away to make it work. // Windows doesn't seem to be able to find stuff like // 'file://..\LWO\LWO2\MappingModes\earthSpherical.jpg' - if (0 == strncmp(ss.data,"file://",7)) + if (0 == strncmp(ss.data, "file://", 7)) { ss.length -= 7; - memmove(ss.data,ss.data+7,ss.length); + memmove(ss.data, ss.data + 7, ss.length); ss.data[ss.length] = '\0'; } - // Maxon Cinema Collada Export writes "file:///C:\andsoon" with three slashes... - // I need to filter it without destroying linux paths starting with "/somewhere" + // Maxon Cinema Collada Export writes "file:///C:\andsoon" with three slashes... + // I need to filter it without destroying linux paths starting with "/somewhere" #if defined( _MSC_VER ) - if( ss.data[0] == '/' && isalpha( (unsigned char) ss.data[1]) && ss.data[2] == ':' ) { + if (ss.data[0] == '/' && isalpha((unsigned char)ss.data[1]) && ss.data[2] == ':') { #else - if (ss.data[ 0 ] == '/' && isalpha( ss.data[ 1 ] ) && ss.data[ 2 ] == ':') { + if (ss.data[0] == '/' && isalpha(ss.data[1]) && ss.data[2] == ':') { #endif - --ss.length; - ::memmove( ss.data, ss.data+1, ss.length); - ss.data[ss.length] = 0; - } - - // find and convert all %xy special chars - char* out = ss.data; - for( const char* it = ss.data; it != ss.data + ss.length; /**/ ) - { - if( *it == '%' && (it + 3) < ss.data + ss.length ) - { - // separate the number to avoid dragging in chars from behind into the parsing - char mychar[3] = { it[1], it[2], 0 }; - size_t nbr = strtoul16( mychar); - it += 3; - *out++ = (char)(nbr & 0xFF); - } else - { - *out++ = *it++; + --ss.length; + ::memmove(ss.data, ss.data + 1, ss.length); + ss.data[ss.length] = 0; } - } - // adjust length and terminator of the shortened string - *out = 0; - ss.length = (ptrdiff_t) (out - ss.data); + // find and convert all %xy special chars + char* out = ss.data; + for (const char* it = ss.data; it != ss.data + ss.length; /**/) + { + if (*it == '%' && (it + 3) < ss.data + ss.length) + { + // separate the number to avoid dragging in chars from behind into the parsing + char mychar[3] = { it[1], it[2], 0 }; + size_t nbr = strtoul16(mychar); + it += 3; + *out++ = (char)(nbr & 0xFF); + } + else + { + *out++ = *it++; + } + } + + // adjust length and terminator of the shortened string + *out = 0; + ss.length = (ptrdiff_t)(out - ss.data); } // ------------------------------------------------------------------------------------------------ // Reads a float value from an accessor and its data array. -ai_real ColladaLoader::ReadFloat( const Collada::Accessor& pAccessor, const Collada::Data& pData, size_t pIndex, size_t pOffset) const +ai_real ColladaLoader::ReadFloat(const Collada::Accessor& pAccessor, const Collada::Data& pData, size_t pIndex, size_t pOffset) const { // FIXME: (thom) Test for data type here in every access? For the moment, I leave this to the caller size_t pos = pAccessor.mStride * pIndex + pAccessor.mOffset + pOffset; - ai_assert( pos < pData.mValues.size()); + ai_assert(pos < pData.mValues.size()); return pData.mValues[pos]; } // ------------------------------------------------------------------------------------------------ // Reads a string value from an accessor and its data array. -const std::string& ColladaLoader::ReadString( const Collada::Accessor& pAccessor, const Collada::Data& pData, size_t pIndex) const +const std::string& ColladaLoader::ReadString(const Collada::Accessor& pAccessor, const Collada::Data& pData, size_t pIndex) const { size_t pos = pAccessor.mStride * pIndex + pAccessor.mOffset; - ai_assert( pos < pData.mStrings.size()); + ai_assert(pos < pData.mStrings.size()); return pData.mStrings[pos]; } // ------------------------------------------------------------------------------------------------ // Collects all nodes into the given array -void ColladaLoader::CollectNodes( const aiNode* pNode, std::vector& poNodes) const +void ColladaLoader::CollectNodes(const aiNode* pNode, std::vector& poNodes) const { - poNodes.push_back( pNode); + poNodes.push_back(pNode); for (size_t a = 0; a < pNode->mNumChildren; ++a) { CollectNodes(pNode->mChildren[a], poNodes); } @@ -1863,15 +1887,15 @@ void ColladaLoader::CollectNodes( const aiNode* pNode, std::vectormName == pName || pNode->mID == pName) + if (pNode->mName == pName || pNode->mID == pName) return pNode; - for( size_t a = 0; a < pNode->mChildren.size(); ++a) + for (size_t a = 0; a < pNode->mChildren.size(); ++a) { - const Collada::Node* node = FindNode( pNode->mChildren[a], pName); - if( node) + const Collada::Node* node = FindNode(pNode->mChildren[a], pName); + if (node) return node; } @@ -1880,32 +1904,33 @@ const Collada::Node* ColladaLoader::FindNode( const Collada::Node* pNode, const // ------------------------------------------------------------------------------------------------ // Finds a node in the collada scene by the given SID -const Collada::Node* ColladaLoader::FindNodeBySID( const Collada::Node* pNode, const std::string& pSID) const +const Collada::Node* ColladaLoader::FindNodeBySID(const Collada::Node* pNode, const std::string& pSID) const { - if( pNode->mSID == pSID) - return pNode; + if (pNode->mSID == pSID) + return pNode; - for( size_t a = 0; a < pNode->mChildren.size(); ++a) - { - const Collada::Node* node = FindNodeBySID( pNode->mChildren[a], pSID); - if( node) - return node; - } + for (size_t a = 0; a < pNode->mChildren.size(); ++a) + { + const Collada::Node* node = FindNodeBySID(pNode->mChildren[a], pSID); + if (node) + return node; + } - return NULL; + return NULL; } // ------------------------------------------------------------------------------------------------ // Finds a proper unique name for a node derived from the collada-node's properties. // The name must be unique for proper node-bone association. -std::string ColladaLoader::FindNameForNode( const Collada::Node* pNode) +std::string ColladaLoader::FindNameForNode(const Collada::Node* pNode) { // If explicitly requested, just use the collada name. if (useColladaName) { if (!pNode->mName.empty()) { return pNode->mName; - } else { + } + else { return format() << "$ColladaAutoName$_" << mNodeNameCounter++; } } diff --git a/code/Collada/ColladaLoader.h b/code/Collada/ColladaLoader.h index 72c2dd8e7..ad606ea74 100644 --- a/code/Collada/ColladaLoader.h +++ b/code/Collada/ColladaLoader.h @@ -94,7 +94,7 @@ public: public: /** Returns whether the class can handle the format of the given file. * See BaseImporter::CanRead() for details. */ - bool CanRead( const std::string& pFile, IOSystem* pIOHandler, bool checkSig) const; + bool CanRead( const std::string& pFile, IOSystem* pIOHandler, bool checkSig) const override; protected: /** Return importer meta information. diff --git a/code/Collada/ColladaParser.cpp b/code/Collada/ColladaParser.cpp index 860ae2ae9..560c0cc9d 100644 --- a/code/Collada/ColladaParser.cpp +++ b/code/Collada/ColladaParser.cpp @@ -57,6 +57,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include +#include #include @@ -66,9 +67,9 @@ using namespace Assimp::Formatter; // ------------------------------------------------------------------------------------------------ // Constructor to be privately used by Importer -ColladaParser::ColladaParser( IOSystem* pIOHandler, const std::string& pFile) - : mFileName( pFile ) - , mReader( nullptr ) +ColladaParser::ColladaParser(IOSystem* pIOHandler, const std::string& pFile) + : mFileName(pFile) + , mReader(nullptr) , mDataLibrary() , mAccessorLibrary() , mMeshLibrary() @@ -79,32 +80,60 @@ ColladaParser::ColladaParser( IOSystem* pIOHandler, const std::string& pFile) , mLightLibrary() , mCameraLibrary() , mControllerLibrary() - , mRootNode( nullptr ) + , mRootNode(nullptr) , mAnims() - , mUnitSize( 1.0f ) - , mUpDirection( UP_Y ) - , mFormat(FV_1_5_n ) // We assume the newest file format by default + , mUnitSize(1.0f) + , mUpDirection(UP_Y) + , mFormat(FV_1_5_n) // We assume the newest file format by default { // validate io-handler instance - if (nullptr == pIOHandler ) { - throw DeadlyImportError("IOSystem is NULL." ); + if (nullptr == pIOHandler) { + throw DeadlyImportError("IOSystem is NULL."); } - // open the file - std::unique_ptr file( pIOHandler->Open(pFile ) ); - if (file.get() == nullptr) { - throw DeadlyImportError( "Failed to open file " + pFile + "." ); + std::unique_ptr daefile; + std::unique_ptr zip_archive; + + // Determine type + std::string extension = BaseImporter::GetExtension(pFile); + if (extension != "dae") { + zip_archive.reset(new ZipArchiveIOSystem(pIOHandler, pFile)); + } + + if (zip_archive && zip_archive->isOpen()) { + std::string dae_filename = ReadZaeManifest(*zip_archive); + + if (dae_filename.empty()) { + ThrowException(std::string("Invalid ZAE")); + } + + daefile.reset(zip_archive->Open(dae_filename.c_str())); + if (daefile == nullptr) { + ThrowException(std::string("Invalid ZAE manifest: '") + std::string(dae_filename) + std::string("' is missing")); + } + } + else { + // attempt to open the file directly + daefile.reset(pIOHandler->Open(pFile)); + if (daefile.get() == nullptr) { + throw DeadlyImportError("Failed to open file '" + pFile + "'."); + } } // generate a XML reader for it - std::unique_ptr mIOWrapper(new CIrrXML_IOStreamReader(file.get())); - mReader = irr::io::createIrrXMLReader( mIOWrapper.get()); + std::unique_ptr mIOWrapper(new CIrrXML_IOStreamReader(daefile.get())); + mReader = irr::io::createIrrXMLReader(mIOWrapper.get()); if (!mReader) { - ThrowException("Collada: Unable to open file."); + ThrowException("Unable to read file, malformed XML"); } // start reading ReadContents(); + + // read embedded textures + if (zip_archive && zip_archive->isOpen()) { + // TODO + } } // ------------------------------------------------------------------------------------------------ @@ -112,18 +141,61 @@ ColladaParser::ColladaParser( IOSystem* pIOHandler, const std::string& pFile) ColladaParser::~ColladaParser() { delete mReader; - for( NodeLibrary::iterator it = mNodeLibrary.begin(); it != mNodeLibrary.end(); ++it) + for (NodeLibrary::iterator it = mNodeLibrary.begin(); it != mNodeLibrary.end(); ++it) delete it->second; - for( MeshLibrary::iterator it = mMeshLibrary.begin(); it != mMeshLibrary.end(); ++it) + for (MeshLibrary::iterator it = mMeshLibrary.begin(); it != mMeshLibrary.end(); ++it) delete it->second; } +// ------------------------------------------------------------------------------------------------ +// Read a ZAE manifest and return the filename to attempt to open +std::string ColladaParser::ReadZaeManifest(ZipArchiveIOSystem &zip_archive) { + // Open the manifest + std::unique_ptr manifestfile(zip_archive.Open("manifest.xml")); + if (manifestfile == nullptr) + { + // No manifest, hope there is only one .DAE inside + std::vector file_list; + zip_archive.getFileListExtension(file_list, "dae"); + + if (file_list.empty()) + return std::string(); + + return file_list.front(); + } + + std::unique_ptr mIOWrapper(new CIrrXML_IOStreamReader(manifestfile.get())); + irr::io::IrrXMLReader* manifest_reader = irr::io::createIrrXMLReader(mIOWrapper.get()); + + while (manifest_reader->read()) + { + // find the manifest "dae_root" element + if (manifest_reader->getNodeType() == irr::io::EXN_ELEMENT) + { + if (::strcmp(manifest_reader->getNodeName(), "dae_root") == 0) + { + if (!manifest_reader->read()) + return std::string(); + if (manifest_reader->getNodeType() != irr::io::EXN_TEXT && manifest_reader->getNodeType() != irr::io::EXN_CDATA) + return std::string(); + + const char* filepath = manifest_reader->getNodeData(); + if (filepath == nullptr) + return std::string(); + + return std::string(filepath); + } + } + } + return std::string(); +} + // ------------------------------------------------------------------------------------------------ // Read bool from text contents of current element bool ColladaParser::ReadBoolFromTextContent() { const char* cur = GetTextContent(); - return (!ASSIMP_strincmp(cur,"true",4) || '0' != *cur); + return (!ASSIMP_strincmp(cur, "true", 4) || '0' != *cur); } // ------------------------------------------------------------------------------------------------ @@ -138,39 +210,41 @@ ai_real ColladaParser::ReadFloatFromTextContent() // Reads the contents of the file void ColladaParser::ReadContents() { - while( mReader->read()) + while (mReader->read()) { // handle the root element "COLLADA" - if( mReader->getNodeType() == irr::io::EXN_ELEMENT) + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) { - if( IsElement( "COLLADA")) + if (IsElement("COLLADA")) { // check for 'version' attribute const int attrib = TestAttribute("version"); if (attrib != -1) { const char* version = mReader->getAttributeValue(attrib); - if (!::strncmp(version,"1.5",3)) { - mFormat = FV_1_5_n; + if (!::strncmp(version, "1.5", 3)) { + mFormat = FV_1_5_n; ASSIMP_LOG_DEBUG("Collada schema version is 1.5.n"); } - else if (!::strncmp(version,"1.4",3)) { - mFormat = FV_1_4_n; + else if (!::strncmp(version, "1.4", 3)) { + mFormat = FV_1_4_n; ASSIMP_LOG_DEBUG("Collada schema version is 1.4.n"); } - else if (!::strncmp(version,"1.3",3)) { - mFormat = FV_1_3_n; + else if (!::strncmp(version, "1.3", 3)) { + mFormat = FV_1_3_n; ASSIMP_LOG_DEBUG("Collada schema version is 1.3.n"); } } ReadStructure(); - } else + } + else { - ASSIMP_LOG_DEBUG_F( "Ignoring global element <", mReader->getNodeName(), ">." ); + ASSIMP_LOG_DEBUG_F("Ignoring global element <", mReader->getNodeName(), ">."); SkipElement(); } - } else + } + else { // skip everything else silently } @@ -181,47 +255,47 @@ void ColladaParser::ReadContents() // Reads the structure of the file void ColladaParser::ReadStructure() { - while( mReader->read()) + while (mReader->read()) { // beginning of elements - if( mReader->getNodeType() == irr::io::EXN_ELEMENT) + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) { - if( IsElement( "asset")) + if (IsElement("asset")) ReadAssetInfo(); - else if( IsElement( "library_animations")) + else if (IsElement("library_animations")) ReadAnimationLibrary(); - else if (IsElement("library_animation_clips")) - ReadAnimationClipLibrary(); - else if( IsElement( "library_controllers")) + else if (IsElement("library_animation_clips")) + ReadAnimationClipLibrary(); + else if (IsElement("library_controllers")) ReadControllerLibrary(); - else if( IsElement( "library_images")) + else if (IsElement("library_images")) ReadImageLibrary(); - else if( IsElement( "library_materials")) + else if (IsElement("library_materials")) ReadMaterialLibrary(); - else if( IsElement( "library_effects")) + else if (IsElement("library_effects")) ReadEffectLibrary(); - else if( IsElement( "library_geometries")) + else if (IsElement("library_geometries")) ReadGeometryLibrary(); - else if( IsElement( "library_visual_scenes")) + else if (IsElement("library_visual_scenes")) ReadSceneLibrary(); - else if( IsElement( "library_lights")) + else if (IsElement("library_lights")) ReadLightLibrary(); - else if( IsElement( "library_cameras")) + else if (IsElement("library_cameras")) ReadCameraLibrary(); - else if( IsElement( "library_nodes")) + else if (IsElement("library_nodes")) ReadSceneNode(NULL); /* some hacking to reuse this piece of code */ - else if( IsElement( "scene")) + else if (IsElement("scene")) ReadScene(); else SkipElement(); } - else if( mReader->getNodeType() == irr::io::EXN_ELEMENT_END) + else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { break; } } - PostProcessRootAnimations(); + PostProcessRootAnimations(); PostProcessControllers(); } @@ -229,43 +303,43 @@ void ColladaParser::ReadStructure() // Reads asset information such as coordinate system information and legal blah void ColladaParser::ReadAssetInfo() { - if( mReader->isEmptyElement()) + if (mReader->isEmptyElement()) return; - while( mReader->read()) + while (mReader->read()) { - if( mReader->getNodeType() == irr::io::EXN_ELEMENT) + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) { - if( IsElement( "unit")) + if (IsElement("unit")) { // read unit data from the element's attributes - const int attrIndex = TestAttribute( "meter"); + const int attrIndex = TestAttribute("meter"); if (attrIndex == -1) { mUnitSize = 1.f; } else { - mUnitSize = mReader->getAttributeValueAsFloat( attrIndex); + mUnitSize = mReader->getAttributeValueAsFloat(attrIndex); } // consume the trailing stuff - if( !mReader->isEmptyElement()) + if (!mReader->isEmptyElement()) SkipElement(); } - else if( IsElement( "up_axis")) + else if (IsElement("up_axis")) { // read content, strip whitespace, compare const char* content = GetTextContent(); - if( strncmp( content, "X_UP", 4) == 0) + if (strncmp(content, "X_UP", 4) == 0) mUpDirection = UP_X; - else if( strncmp( content, "Z_UP", 4) == 0) + else if (strncmp(content, "Z_UP", 4) == 0) mUpDirection = UP_Z; else mUpDirection = UP_Y; // check element end - TestClosing( "up_axis"); + TestClosing("up_axis"); } - else if(IsElement("contributor")) + else if (IsElement("contributor")) { ReadContributorInfo(); } @@ -274,10 +348,10 @@ void ColladaParser::ReadAssetInfo() ReadMetaDataItem(mAssetMetaData); } } - else if( mReader->getNodeType() == irr::io::EXN_ELEMENT_END) + else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { - if (strcmp( mReader->getNodeName(), "asset") != 0) - ThrowException( "Expected end of element."); + if (strcmp(mReader->getNodeName(), "asset") != 0) + ThrowException("Expected end of element."); break; } @@ -355,82 +429,82 @@ void ColladaParser::ToCamelCase(std::string &text) // Reads the animation clips void ColladaParser::ReadAnimationClipLibrary() { - if (mReader->isEmptyElement()) - return; + if (mReader->isEmptyElement()) + return; - while (mReader->read()) - { - if (mReader->getNodeType() == irr::io::EXN_ELEMENT) - { - if (IsElement("animation_clip")) - { - // optional name given as an attribute - std::string animName; - int indexName = TestAttribute("name"); - int indexID = TestAttribute("id"); - if (indexName >= 0) - animName = mReader->getAttributeValue(indexName); - else if (indexID >= 0) - animName = mReader->getAttributeValue(indexID); - else - animName = std::string("animation_") + to_string(mAnimationClipLibrary.size()); + while (mReader->read()) + { + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) + { + if (IsElement("animation_clip")) + { + // optional name given as an attribute + std::string animName; + int indexName = TestAttribute("name"); + int indexID = TestAttribute("id"); + if (indexName >= 0) + animName = mReader->getAttributeValue(indexName); + else if (indexID >= 0) + animName = mReader->getAttributeValue(indexID); + else + animName = std::string("animation_") + to_string(mAnimationClipLibrary.size()); - std::pair > clip; + std::pair > clip; - clip.first = animName; + clip.first = animName; - while (mReader->read()) - { - if (mReader->getNodeType() == irr::io::EXN_ELEMENT) - { - if (IsElement("instance_animation")) - { - int indexUrl = TestAttribute("url"); - if (indexUrl >= 0) - { - const char* url = mReader->getAttributeValue(indexUrl); - if (url[0] != '#') - ThrowException("Unknown reference format"); + while (mReader->read()) + { + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) + { + if (IsElement("instance_animation")) + { + int indexUrl = TestAttribute("url"); + if (indexUrl >= 0) + { + const char* url = mReader->getAttributeValue(indexUrl); + if (url[0] != '#') + ThrowException("Unknown reference format"); - url++; + url++; - clip.second.push_back(url); - } - } - else - { - // ignore the rest - SkipElement(); - } - } - else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) - { - if (strcmp(mReader->getNodeName(), "animation_clip") != 0) - ThrowException("Expected end of element."); + clip.second.push_back(url); + } + } + else + { + // ignore the rest + SkipElement(); + } + } + else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) + { + if (strcmp(mReader->getNodeName(), "animation_clip") != 0) + ThrowException("Expected end of element."); - break; - } - } + break; + } + } - if (clip.second.size() > 0) - { - mAnimationClipLibrary.push_back(clip); - } - } - else - { - // ignore the rest - SkipElement(); - } - } - else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) - { - if (strcmp(mReader->getNodeName(), "library_animation_clips") != 0) - ThrowException("Expected end of element."); + if (clip.second.size() > 0) + { + mAnimationClipLibrary.push_back(clip); + } + } + else + { + // ignore the rest + SkipElement(); + } + } + else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) + { + if (strcmp(mReader->getNodeName(), "library_animation_clips") != 0) + ThrowException("Expected end of element."); - break; - } - } + break; + } + } } void ColladaParser::PostProcessControllers() @@ -439,11 +513,11 @@ void ColladaParser::PostProcessControllers() for (ControllerLibrary::iterator it = mControllerLibrary.begin(); it != mControllerLibrary.end(); ++it) { meshId = it->second.mMeshId; ControllerLibrary::iterator findItr = mControllerLibrary.find(meshId); - while(findItr != mControllerLibrary.end()) { + while (findItr != mControllerLibrary.end()) { meshId = findItr->second.mMeshId; findItr = mControllerLibrary.find(meshId); } - + it->second.mMeshId = meshId; } } @@ -452,43 +526,43 @@ void ColladaParser::PostProcessControllers() // Re-build animations from animation clip library, if present, otherwise combine single-channel animations void ColladaParser::PostProcessRootAnimations() { - if (mAnimationClipLibrary.size() > 0) - { - Animation temp; + if (mAnimationClipLibrary.size() > 0) + { + Animation temp; - for (AnimationClipLibrary::iterator it = mAnimationClipLibrary.begin(); it != mAnimationClipLibrary.end(); ++it) - { - std::string clipName = it->first; + for (AnimationClipLibrary::iterator it = mAnimationClipLibrary.begin(); it != mAnimationClipLibrary.end(); ++it) + { + std::string clipName = it->first; - Animation *clip = new Animation(); - clip->mName = clipName; + Animation *clip = new Animation(); + clip->mName = clipName; - temp.mSubAnims.push_back(clip); + temp.mSubAnims.push_back(clip); - for (std::vector::iterator a = it->second.begin(); a != it->second.end(); ++a) - { - std::string animationID = *a; + for (std::vector::iterator a = it->second.begin(); a != it->second.end(); ++a) + { + std::string animationID = *a; - AnimationLibrary::iterator animation = mAnimationLibrary.find(animationID); + AnimationLibrary::iterator animation = mAnimationLibrary.find(animationID); - if (animation != mAnimationLibrary.end()) - { - Animation *pSourceAnimation = animation->second; + if (animation != mAnimationLibrary.end()) + { + Animation *pSourceAnimation = animation->second; - pSourceAnimation->CollectChannelsRecursively(clip->mChannels); - } - } - } + pSourceAnimation->CollectChannelsRecursively(clip->mChannels); + } + } + } - mAnims = temp; + mAnims = temp; - // Ensure no double deletes. - temp.mSubAnims.clear(); - } - else - { - mAnims.CombineSingleChannelAnimations(); - } + // Ensure no double deletes. + temp.mSubAnims.clear(); + } + else + { + mAnims.CombineSingleChannelAnimations(); + } } // ------------------------------------------------------------------------------------------------ @@ -498,24 +572,25 @@ void ColladaParser::ReadAnimationLibrary() if (mReader->isEmptyElement()) return; - while( mReader->read()) + while (mReader->read()) { - if( mReader->getNodeType() == irr::io::EXN_ELEMENT) + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) { - if( IsElement( "animation")) + if (IsElement("animation")) { // delegate the reading. Depending on the inner elements it will be a container or a anim channel - ReadAnimation( &mAnims); - } else + ReadAnimation(&mAnims); + } + else { // ignore the rest SkipElement(); } } - else if( mReader->getNodeType() == irr::io::EXN_ELEMENT_END) + else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { - if( strcmp( mReader->getNodeName(), "library_animations") != 0) - ThrowException( "Expected end of element."); + if (strcmp(mReader->getNodeName(), "library_animations") != 0) + ThrowException("Expected end of element."); break; } @@ -524,9 +599,9 @@ void ColladaParser::ReadAnimationLibrary() // ------------------------------------------------------------------------------------------------ // Reads an animation into the given parent structure -void ColladaParser::ReadAnimation( Collada::Animation* pParent) +void ColladaParser::ReadAnimation(Collada::Animation* pParent) { - if( mReader->isEmptyElement()) + if (mReader->isEmptyElement()) return; // an element may be a container for grouping sub-elements or an animation channel @@ -538,68 +613,68 @@ void ColladaParser::ReadAnimation( Collada::Animation* pParent) // optional name given as an attribute std::string animName; - std::string animID; - int indexName = TestAttribute( "name"); - int indexID = TestAttribute( "id"); + std::string animID; + int indexName = TestAttribute("name"); + int indexID = TestAttribute("id"); - if (indexID >= 0) - animID = mReader->getAttributeValue(indexID); + if (indexID >= 0) + animID = mReader->getAttributeValue(indexID); - if( indexName >= 0) - animName = mReader->getAttributeValue( indexName); - else if( indexID >= 0) + if (indexName >= 0) + animName = mReader->getAttributeValue(indexName); + else if (indexID >= 0) animName = animID; else animName = "animation"; - while( mReader->read()) + while (mReader->read()) { - if( mReader->getNodeType() == irr::io::EXN_ELEMENT) + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) { // we have subanimations - if( IsElement( "animation")) + if (IsElement("animation")) { // create container from our element - if( !anim) + if (!anim) { anim = new Animation; anim->mName = animName; - pParent->mSubAnims.push_back( anim); + pParent->mSubAnims.push_back(anim); } // recurse into the subelement - ReadAnimation( anim); + ReadAnimation(anim); } - else if( IsElement( "source")) + else if (IsElement("source")) { // possible animation data - we'll never know. Better store it ReadSource(); } - else if( IsElement( "sampler")) + else if (IsElement("sampler")) { // read the ID to assign the corresponding collada channel afterwards. - int indexID = GetAttribute( "id"); - std::string id = mReader->getAttributeValue( indexID); - ChannelMap::iterator newChannel = channels.insert( std::make_pair( id, AnimationChannel())).first; + int indexID = GetAttribute("id"); + std::string id = mReader->getAttributeValue(indexID); + ChannelMap::iterator newChannel = channels.insert(std::make_pair(id, AnimationChannel())).first; // have it read into a channel - ReadAnimationSampler( newChannel->second); + ReadAnimationSampler(newChannel->second); } - else if( IsElement( "channel")) + else if (IsElement("channel")) { // the binding element whose whole purpose is to provide the target to animate // Thanks, Collada! A directly posted information would have been too simple, I guess. // Better add another indirection to that! Can't have enough of those. - int indexTarget = GetAttribute( "target"); - int indexSource = GetAttribute( "source"); - const char* sourceId = mReader->getAttributeValue( indexSource); - if( sourceId[0] == '#') + int indexTarget = GetAttribute("target"); + int indexSource = GetAttribute("source"); + const char* sourceId = mReader->getAttributeValue(indexSource); + if (sourceId[0] == '#') sourceId++; - ChannelMap::iterator cit = channels.find( sourceId); - if( cit != channels.end()) - cit->second.mTarget = mReader->getAttributeValue( indexTarget); + ChannelMap::iterator cit = channels.find(sourceId); + if (cit != channels.end()) + cit->second.mTarget = mReader->getAttributeValue(indexTarget); - if( !mReader->isEmptyElement()) + if (!mReader->isEmptyElement()) SkipElement(); } else @@ -608,24 +683,24 @@ void ColladaParser::ReadAnimation( Collada::Animation* pParent) SkipElement(); } } - else if( mReader->getNodeType() == irr::io::EXN_ELEMENT_END) + else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { - if( strcmp( mReader->getNodeName(), "animation") != 0) - ThrowException( "Expected end of element."); + if (strcmp(mReader->getNodeName(), "animation") != 0) + ThrowException("Expected end of element."); break; } } // it turned out to have channels - add them - if( !channels.empty()) + if (!channels.empty()) { - // FIXME: Is this essentially doing the same as "single-anim-node" codepath in - // ColladaLoader::StoreAnimations? For now, this has been deferred to after - // all animations and all clips have been read. Due to handling of - // this cannot be done here, as the channel owner - // is lost, and some exporters make up animations by referring to multiple - // single-channel animations from an . + // FIXME: Is this essentially doing the same as "single-anim-node" codepath in + // ColladaLoader::StoreAnimations? For now, this has been deferred to after + // all animations and all clips have been read. Due to handling of + // this cannot be done here, as the channel owner + // is lost, and some exporters make up animations by referring to multiple + // single-channel animations from an . /* // special filtering for stupid exporters packing each channel into a separate animation if( channels.size() == 1) @@ -635,53 +710,53 @@ void ColladaParser::ReadAnimation( Collada::Animation* pParent) */ { // else create the animation, if not done yet, and store the channels - if( !anim) + if (!anim) { anim = new Animation; anim->mName = animName; - pParent->mSubAnims.push_back( anim); + pParent->mSubAnims.push_back(anim); } - for( ChannelMap::const_iterator it = channels.begin(); it != channels.end(); ++it) - anim->mChannels.push_back( it->second); + for (ChannelMap::const_iterator it = channels.begin(); it != channels.end(); ++it) + anim->mChannels.push_back(it->second); - if (indexID >= 0) - { - mAnimationLibrary[animID] = anim; - } + if (indexID >= 0) + { + mAnimationLibrary[animID] = anim; + } } } } // ------------------------------------------------------------------------------------------------ // Reads an animation sampler into the given anim channel -void ColladaParser::ReadAnimationSampler( Collada::AnimationChannel& pChannel) +void ColladaParser::ReadAnimationSampler(Collada::AnimationChannel& pChannel) { - while( mReader->read()) + while (mReader->read()) { - if( mReader->getNodeType() == irr::io::EXN_ELEMENT) + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) { - if( IsElement( "input")) + if (IsElement("input")) { - int indexSemantic = GetAttribute( "semantic"); - const char* semantic = mReader->getAttributeValue( indexSemantic); - int indexSource = GetAttribute( "source"); - const char* source = mReader->getAttributeValue( indexSource); - if( source[0] != '#') - ThrowException( "Unsupported URL format"); + int indexSemantic = GetAttribute("semantic"); + const char* semantic = mReader->getAttributeValue(indexSemantic); + int indexSource = GetAttribute("source"); + const char* source = mReader->getAttributeValue(indexSource); + if (source[0] != '#') + ThrowException("Unsupported URL format"); source++; - if( strcmp( semantic, "INPUT") == 0) + if (strcmp(semantic, "INPUT") == 0) pChannel.mSourceTimes = source; - else if( strcmp( semantic, "OUTPUT") == 0) + else if (strcmp(semantic, "OUTPUT") == 0) pChannel.mSourceValues = source; - else if( strcmp( semantic, "IN_TANGENT") == 0) + else if (strcmp(semantic, "IN_TANGENT") == 0) pChannel.mInTanValues = source; - else if( strcmp( semantic, "OUT_TANGENT") == 0) + else if (strcmp(semantic, "OUT_TANGENT") == 0) pChannel.mOutTanValues = source; - else if( strcmp( semantic, "INTERPOLATION") == 0) + else if (strcmp(semantic, "INTERPOLATION") == 0) pChannel.mInterpolationValues = source; - if( !mReader->isEmptyElement()) + if (!mReader->isEmptyElement()) SkipElement(); } else @@ -690,10 +765,10 @@ void ColladaParser::ReadAnimationSampler( Collada::AnimationChannel& pChannel) SkipElement(); } } - else if( mReader->getNodeType() == irr::io::EXN_ELEMENT_END) + else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { - if( strcmp( mReader->getNodeName(), "sampler") != 0) - ThrowException( "Expected end of element."); + if (strcmp(mReader->getNodeName(), "sampler") != 0) + ThrowException("Expected end of element."); break; } @@ -707,31 +782,32 @@ void ColladaParser::ReadControllerLibrary() if (mReader->isEmptyElement()) return; - while( mReader->read()) + while (mReader->read()) { - if( mReader->getNodeType() == irr::io::EXN_ELEMENT) + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) { - if( IsElement( "controller")) + if (IsElement("controller")) { // read ID. Ask the spec if it's necessary or optional... you might be surprised. - int attrID = GetAttribute( "id"); - std::string id = mReader->getAttributeValue( attrID); + int attrID = GetAttribute("id"); + std::string id = mReader->getAttributeValue(attrID); // create an entry and store it in the library under its ID mControllerLibrary[id] = Controller(); // read on from there - ReadController( mControllerLibrary[id]); - } else + ReadController(mControllerLibrary[id]); + } + else { // ignore the rest SkipElement(); } } - else if( mReader->getNodeType() == irr::io::EXN_ELEMENT_END) + else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { - if( strcmp( mReader->getNodeName(), "library_controllers") != 0) - ThrowException( "Expected end of element."); + if (strcmp(mReader->getNodeName(), "library_controllers") != 0) + ThrowException("Expected end of element."); break; } @@ -740,17 +816,17 @@ void ColladaParser::ReadControllerLibrary() // ------------------------------------------------------------------------------------------------ // Reads a controller into the given mesh structure -void ColladaParser::ReadController( Collada::Controller& pController) +void ColladaParser::ReadController(Collada::Controller& pController) { // initial values pController.mType = Skin; pController.mMethod = Normalized; - while( mReader->read()) + while (mReader->read()) { - if( mReader->getNodeType() == irr::io::EXN_ELEMENT) + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) { // two types of controllers: "skin" and "morph". Only the first one is relevant, we skip the other - if( IsElement( "morph")) + if (IsElement("morph")) { pController.mType = Morph; int baseIndex = GetAttribute("source"); @@ -762,47 +838,47 @@ void ColladaParser::ReadController( Collada::Controller& pController) pController.mMethod = Relative; } } - else if( IsElement( "skin")) + else if (IsElement("skin")) { // read the mesh it refers to. According to the spec this could also be another // controller, but I refuse to implement every single idea they've come up with - int sourceIndex = GetAttribute( "source"); - pController.mMeshId = mReader->getAttributeValue( sourceIndex) + 1; + int sourceIndex = GetAttribute("source"); + pController.mMeshId = mReader->getAttributeValue(sourceIndex) + 1; } - else if( IsElement( "bind_shape_matrix")) + else if (IsElement("bind_shape_matrix")) { // content is 16 floats to define a matrix... it seems to be important for some models const char* content = GetTextContent(); // read the 16 floats - for( unsigned int a = 0; a < 16; a++) + for (unsigned int a = 0; a < 16; a++) { // read a number - content = fast_atoreal_move( content, pController.mBindShapeMatrix[a]); + content = fast_atoreal_move(content, pController.mBindShapeMatrix[a]); // skip whitespace after it - SkipSpacesAndLineEnd( &content); + SkipSpacesAndLineEnd(&content); } - TestClosing( "bind_shape_matrix"); + TestClosing("bind_shape_matrix"); } - else if( IsElement( "source")) + else if (IsElement("source")) { // data array - we have specialists to handle this ReadSource(); } - else if( IsElement( "joints")) + else if (IsElement("joints")) { - ReadControllerJoints( pController); + ReadControllerJoints(pController); } - else if( IsElement( "vertex_weights")) + else if (IsElement("vertex_weights")) { - ReadControllerWeights( pController); + ReadControllerWeights(pController); } - else if ( IsElement( "targets" )) + else if (IsElement("targets")) { while (mReader->read()) { - if( mReader->getNodeType() == irr::io::EXN_ELEMENT) { - if ( IsElement( "input")) { + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) { + if (IsElement("input")) { int semanticsIndex = GetAttribute("semantic"); int sourceIndex = GetAttribute("source"); @@ -816,11 +892,12 @@ void ColladaParser::ReadController( Collada::Controller& pController) pController.mMorphWeight = source + 1; } } - } else if( mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { - if( strcmp( mReader->getNodeName(), "targets") == 0) + } + else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { + if (strcmp(mReader->getNodeName(), "targets") == 0) break; else - ThrowException( "Expected end of element."); + ThrowException("Expected end of element."); } } } @@ -830,47 +907,47 @@ void ColladaParser::ReadController( Collada::Controller& pController) SkipElement(); } } - else if( mReader->getNodeType() == irr::io::EXN_ELEMENT_END) + else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { - if( strcmp( mReader->getNodeName(), "controller") == 0) + if (strcmp(mReader->getNodeName(), "controller") == 0) break; - else if( strcmp( mReader->getNodeName(), "skin") != 0 && strcmp( mReader->getNodeName(), "morph") != 0) - ThrowException( "Expected end of element."); + else if (strcmp(mReader->getNodeName(), "skin") != 0 && strcmp(mReader->getNodeName(), "morph") != 0) + ThrowException("Expected end of element."); } } } // ------------------------------------------------------------------------------------------------ // Reads the joint definitions for the given controller -void ColladaParser::ReadControllerJoints( Collada::Controller& pController) +void ColladaParser::ReadControllerJoints(Collada::Controller& pController) { - while( mReader->read()) + while (mReader->read()) { - if( mReader->getNodeType() == irr::io::EXN_ELEMENT) + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) { // Input channels for joint data. Two possible semantics: "JOINT" and "INV_BIND_MATRIX" - if( IsElement( "input")) + if (IsElement("input")) { - int indexSemantic = GetAttribute( "semantic"); - const char* attrSemantic = mReader->getAttributeValue( indexSemantic); - int indexSource = GetAttribute( "source"); - const char* attrSource = mReader->getAttributeValue( indexSource); + int indexSemantic = GetAttribute("semantic"); + const char* attrSemantic = mReader->getAttributeValue(indexSemantic); + int indexSource = GetAttribute("source"); + const char* attrSource = mReader->getAttributeValue(indexSource); // local URLS always start with a '#'. We don't support global URLs - if( attrSource[0] != '#') - ThrowException( format() << "Unsupported URL format in \"" << attrSource << "\" in source attribute of data element" ); + if (attrSource[0] != '#') + ThrowException(format() << "Unsupported URL format in \"" << attrSource << "\" in source attribute of data element"); attrSource++; // parse source URL to corresponding source - if( strcmp( attrSemantic, "JOINT") == 0) + if (strcmp(attrSemantic, "JOINT") == 0) pController.mJointNameSource = attrSource; - else if( strcmp( attrSemantic, "INV_BIND_MATRIX") == 0) + else if (strcmp(attrSemantic, "INV_BIND_MATRIX") == 0) pController.mJointOffsetMatrixSource = attrSource; else - ThrowException( format() << "Unknown semantic \"" << attrSemantic << "\" in data element" ); + ThrowException(format() << "Unknown semantic \"" << attrSemantic << "\" in data element"); // skip inner data, if present - if( !mReader->isEmptyElement()) + if (!mReader->isEmptyElement()) SkipElement(); } else @@ -879,10 +956,10 @@ void ColladaParser::ReadControllerJoints( Collada::Controller& pController) SkipElement(); } } - else if( mReader->getNodeType() == irr::io::EXN_ELEMENT_END) + else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { - if( strcmp( mReader->getNodeName(), "joints") != 0) - ThrowException( "Expected end of element."); + if (strcmp(mReader->getNodeName(), "joints") != 0) + ThrowException("Expected end of element."); break; } @@ -891,85 +968,85 @@ void ColladaParser::ReadControllerJoints( Collada::Controller& pController) // ------------------------------------------------------------------------------------------------ // Reads the joint weights for the given controller -void ColladaParser::ReadControllerWeights( Collada::Controller& pController) +void ColladaParser::ReadControllerWeights(Collada::Controller& pController) { // read vertex count from attributes and resize the array accordingly - int indexCount = GetAttribute( "count"); - size_t vertexCount = (size_t) mReader->getAttributeValueAsInt( indexCount); - pController.mWeightCounts.resize( vertexCount); + int indexCount = GetAttribute("count"); + size_t vertexCount = (size_t)mReader->getAttributeValueAsInt(indexCount); + pController.mWeightCounts.resize(vertexCount); - while( mReader->read()) + while (mReader->read()) { - if( mReader->getNodeType() == irr::io::EXN_ELEMENT) + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) { // Input channels for weight data. Two possible semantics: "JOINT" and "WEIGHT" - if( IsElement( "input") && vertexCount > 0 ) + if (IsElement("input") && vertexCount > 0) { InputChannel channel; - int indexSemantic = GetAttribute( "semantic"); - const char* attrSemantic = mReader->getAttributeValue( indexSemantic); - int indexSource = GetAttribute( "source"); - const char* attrSource = mReader->getAttributeValue( indexSource); - int indexOffset = TestAttribute( "offset"); - if( indexOffset >= 0) - channel.mOffset = mReader->getAttributeValueAsInt( indexOffset); + int indexSemantic = GetAttribute("semantic"); + const char* attrSemantic = mReader->getAttributeValue(indexSemantic); + int indexSource = GetAttribute("source"); + const char* attrSource = mReader->getAttributeValue(indexSource); + int indexOffset = TestAttribute("offset"); + if (indexOffset >= 0) + channel.mOffset = mReader->getAttributeValueAsInt(indexOffset); // local URLS always start with a '#'. We don't support global URLs - if( attrSource[0] != '#') - ThrowException( format() << "Unsupported URL format in \"" << attrSource << "\" in source attribute of data element" ); + if (attrSource[0] != '#') + ThrowException(format() << "Unsupported URL format in \"" << attrSource << "\" in source attribute of data element"); channel.mAccessor = attrSource + 1; // parse source URL to corresponding source - if( strcmp( attrSemantic, "JOINT") == 0) + if (strcmp(attrSemantic, "JOINT") == 0) pController.mWeightInputJoints = channel; - else if( strcmp( attrSemantic, "WEIGHT") == 0) + else if (strcmp(attrSemantic, "WEIGHT") == 0) pController.mWeightInputWeights = channel; else - ThrowException( format() << "Unknown semantic \"" << attrSemantic << "\" in data element" ); + ThrowException(format() << "Unknown semantic \"" << attrSemantic << "\" in data element"); // skip inner data, if present - if( !mReader->isEmptyElement()) + if (!mReader->isEmptyElement()) SkipElement(); } - else if( IsElement( "vcount") && vertexCount > 0 ) + else if (IsElement("vcount") && vertexCount > 0) { // read weight count per vertex const char* text = GetTextContent(); size_t numWeights = 0; - for( std::vector::iterator it = pController.mWeightCounts.begin(); it != pController.mWeightCounts.end(); ++it) + for (std::vector::iterator it = pController.mWeightCounts.begin(); it != pController.mWeightCounts.end(); ++it) { - if( *text == 0) - ThrowException( "Out of data while reading "); + if (*text == 0) + ThrowException("Out of data while reading "); - *it = strtoul10( text, &text); + *it = strtoul10(text, &text); numWeights += *it; - SkipSpacesAndLineEnd( &text); + SkipSpacesAndLineEnd(&text); } - TestClosing( "vcount"); + TestClosing("vcount"); // reserve weight count - pController.mWeights.resize( numWeights); + pController.mWeights.resize(numWeights); } - else if( IsElement( "v") && vertexCount > 0 ) + else if (IsElement("v") && vertexCount > 0) { // read JointIndex - WeightIndex pairs const char* text = GetTextContent(); - for( std::vector< std::pair >::iterator it = pController.mWeights.begin(); it != pController.mWeights.end(); ++it) + for (std::vector< std::pair >::iterator it = pController.mWeights.begin(); it != pController.mWeights.end(); ++it) { - if( *text == 0) - ThrowException( "Out of data while reading "); - it->first = strtoul10( text, &text); - SkipSpacesAndLineEnd( &text); - if( *text == 0) - ThrowException( "Out of data while reading "); - it->second = strtoul10( text, &text); - SkipSpacesAndLineEnd( &text); + if (*text == 0) + ThrowException("Out of data while reading "); + it->first = strtoul10(text, &text); + SkipSpacesAndLineEnd(&text); + if (*text == 0) + ThrowException("Out of data while reading "); + it->second = strtoul10(text, &text); + SkipSpacesAndLineEnd(&text); } - TestClosing( "v"); + TestClosing("v"); } else { @@ -977,10 +1054,10 @@ void ColladaParser::ReadControllerWeights( Collada::Controller& pController) SkipElement(); } } - else if( mReader->getNodeType() == irr::io::EXN_ELEMENT_END) + else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { - if( strcmp( mReader->getNodeName(), "vertex_weights") != 0) - ThrowException( "Expected end of element."); + if (strcmp(mReader->getNodeName(), "vertex_weights") != 0) + ThrowException("Expected end of element."); break; } @@ -991,32 +1068,33 @@ void ColladaParser::ReadControllerWeights( Collada::Controller& pController) // Reads the image library contents void ColladaParser::ReadImageLibrary() { - if( mReader->isEmptyElement()) + if (mReader->isEmptyElement()) return; - while( mReader->read()) + while (mReader->read()) { - if( mReader->getNodeType() == irr::io::EXN_ELEMENT) { - if( IsElement( "image")) + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) { + if (IsElement("image")) { // read ID. Another entry which is "optional" by design but obligatory in reality - int attrID = GetAttribute( "id"); - std::string id = mReader->getAttributeValue( attrID); + int attrID = GetAttribute("id"); + std::string id = mReader->getAttributeValue(attrID); // create an entry and store it in the library under its ID mImageLibrary[id] = Image(); // read on from there - ReadImage( mImageLibrary[id]); - } else + ReadImage(mImageLibrary[id]); + } + else { // ignore the rest SkipElement(); } } - else if( mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { - if( strcmp( mReader->getNodeName(), "library_images") != 0) - ThrowException( "Expected end of element."); + else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { + if (strcmp(mReader->getNodeName(), "library_images") != 0) + ThrowException("Expected end of element."); break; } @@ -1025,16 +1103,16 @@ void ColladaParser::ReadImageLibrary() // ------------------------------------------------------------------------------------------------ // Reads an image entry into the given image -void ColladaParser::ReadImage( Collada::Image& pImage) +void ColladaParser::ReadImage(Collada::Image& pImage) { - while( mReader->read()) + while (mReader->read()) { - if( mReader->getNodeType() == irr::io::EXN_ELEMENT){ + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) { // Need to run different code paths here, depending on the Collada XSD version if (IsElement("image")) { SkipElement(); } - else if( IsElement( "init_from")) + else if (IsElement("init_from")) { if (mFormat == FV_1_4_n) { @@ -1043,7 +1121,7 @@ void ColladaParser::ReadImage( Collada::Image& pImage) // element content is filename - hopefully const char* sz = TestTextContent(); if (sz)pImage.mFileName = sz; - TestClosing( "init_from"); + TestClosing("init_from"); } if (!pImage.mFileName.length()) { pImage.mFileName = "unknown_texture"; @@ -1071,14 +1149,14 @@ void ColladaParser::ReadImage( Collada::Image& pImage) } else if (mFormat == FV_1_5_n) { - if( IsElement( "ref")) + if (IsElement("ref")) { // element content is filename - hopefully const char* sz = TestTextContent(); if (sz)pImage.mFileName = sz; - TestClosing( "ref"); + TestClosing("ref"); } - else if( IsElement( "hex") && !pImage.mFileName.length()) + else if (IsElement("hex") && !pImage.mFileName.length()) { // embedded image. get format const int attrib = TestAttribute("format"); @@ -1093,12 +1171,12 @@ void ColladaParser::ReadImage( Collada::Image& pImage) const char* cur = data; while (!IsSpaceOrNewLine(*cur)) cur++; - const unsigned int size = (unsigned int)(cur-data) * 2; + const unsigned int size = (unsigned int)(cur - data) * 2; pImage.mImageData.resize(size); - for (unsigned int i = 0; i < size;++i) - pImage.mImageData[i] = HexOctetToDecimal(data+(i<<1)); + for (unsigned int i = 0; i < size; ++i) + pImage.mImageData[i] = HexOctetToDecimal(data + (i << 1)); - TestClosing( "hex"); + TestClosing("hex"); } } else @@ -1107,8 +1185,8 @@ void ColladaParser::ReadImage( Collada::Image& pImage) SkipElement(); } } - else if( mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { - if( strcmp( mReader->getNodeName(), "image") == 0) + else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { + if (strcmp(mReader->getNodeName(), "image") == 0) break; } } @@ -1118,36 +1196,36 @@ void ColladaParser::ReadImage( Collada::Image& pImage) // Reads the material library void ColladaParser::ReadMaterialLibrary() { - if( mReader->isEmptyElement()) + if (mReader->isEmptyElement()) return; std::map names; - while( mReader->read()) + while (mReader->read()) { - if( mReader->getNodeType() == irr::io::EXN_ELEMENT) + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) { - if( IsElement( "material")) + if (IsElement("material")) { // read ID. By now you probably know my opinion about this "specification" - int attrID = GetAttribute( "id"); - std::string id = mReader->getAttributeValue( attrID); + int attrID = GetAttribute("id"); + std::string id = mReader->getAttributeValue(attrID); std::string name; int attrName = TestAttribute("name"); if (attrName >= 0) - name = mReader->getAttributeValue( attrName); + name = mReader->getAttributeValue(attrName); // create an entry and store it in the library under its ID mMaterialLibrary[id] = Material(); - if( !name.empty()) + if (!name.empty()) { - std::map::iterator it = names.find( name); - if( it != names.end()) + std::map::iterator it = names.find(name); + if (it != names.end()) { std::ostringstream strStream; strStream << ++it->second; - name.append( " " + strStream.str()); + name.append(" " + strStream.str()); } else { @@ -1157,17 +1235,18 @@ void ColladaParser::ReadMaterialLibrary() mMaterialLibrary[id].mName = name; } - ReadMaterial( mMaterialLibrary[id]); - } else + ReadMaterial(mMaterialLibrary[id]); + } + else { // ignore the rest SkipElement(); } } - else if( mReader->getNodeType() == irr::io::EXN_ELEMENT_END) + else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { - if( strcmp( mReader->getNodeName(), "library_materials") != 0) - ThrowException( "Expected end of element."); + if (strcmp(mReader->getNodeName(), "library_materials") != 0) + ThrowException("Expected end of element."); break; } @@ -1178,30 +1257,31 @@ void ColladaParser::ReadMaterialLibrary() // Reads the light library void ColladaParser::ReadLightLibrary() { - if( mReader->isEmptyElement()) + if (mReader->isEmptyElement()) return; - while( mReader->read()) + while (mReader->read()) { - if( mReader->getNodeType() == irr::io::EXN_ELEMENT) { - if( IsElement( "light")) + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) { + if (IsElement("light")) { // read ID. By now you probably know my opinion about this "specification" - int attrID = GetAttribute( "id"); - std::string id = mReader->getAttributeValue( attrID); + int attrID = GetAttribute("id"); + std::string id = mReader->getAttributeValue(attrID); // create an entry and store it in the library under its ID ReadLight(mLightLibrary[id] = Light()); - } else + } + else { // ignore the rest SkipElement(); } } - else if( mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { - if( strcmp( mReader->getNodeName(), "library_lights") != 0) - ThrowException( "Expected end of element."); + else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { + if (strcmp(mReader->getNodeName(), "library_lights") != 0) + ThrowException("Expected end of element."); break; } @@ -1212,35 +1292,36 @@ void ColladaParser::ReadLightLibrary() // Reads the camera library void ColladaParser::ReadCameraLibrary() { - if( mReader->isEmptyElement()) + if (mReader->isEmptyElement()) return; - while( mReader->read()) + while (mReader->read()) { - if( mReader->getNodeType() == irr::io::EXN_ELEMENT) { - if( IsElement( "camera")) + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) { + if (IsElement("camera")) { // read ID. By now you probably know my opinion about this "specification" - int attrID = GetAttribute( "id"); - std::string id = mReader->getAttributeValue( attrID); + int attrID = GetAttribute("id"); + std::string id = mReader->getAttributeValue(attrID); // create an entry and store it in the library under its ID Camera& cam = mCameraLibrary[id]; - attrID = TestAttribute( "name"); + attrID = TestAttribute("name"); if (attrID != -1) - cam.mName = mReader->getAttributeValue( attrID); + cam.mName = mReader->getAttributeValue(attrID); ReadCamera(cam); - } else + } + else { // ignore the rest SkipElement(); } } - else if( mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { - if( strcmp( mReader->getNodeName(), "library_cameras") != 0) - ThrowException( "Expected end of element."); + else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { + if (strcmp(mReader->getNodeName(), "library_cameras") != 0) + ThrowException("Expected end of element."); break; } @@ -1249,34 +1330,35 @@ void ColladaParser::ReadCameraLibrary() // ------------------------------------------------------------------------------------------------ // Reads a material entry into the given material -void ColladaParser::ReadMaterial( Collada::Material& pMaterial) +void ColladaParser::ReadMaterial(Collada::Material& pMaterial) { - while( mReader->read()) + while (mReader->read()) { - if( mReader->getNodeType() == irr::io::EXN_ELEMENT) { + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) { if (IsElement("material")) { SkipElement(); } - else if( IsElement( "instance_effect")) + else if (IsElement("instance_effect")) { // referred effect by URL - int attrUrl = GetAttribute( "url"); - const char* url = mReader->getAttributeValue( attrUrl); - if( url[0] != '#') - ThrowException( "Unknown reference format"); + int attrUrl = GetAttribute("url"); + const char* url = mReader->getAttributeValue(attrUrl); + if (url[0] != '#') + ThrowException("Unknown reference format"); - pMaterial.mEffect = url+1; + pMaterial.mEffect = url + 1; SkipElement(); - } else + } + else { // ignore the rest SkipElement(); } } - else if( mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { - if( strcmp( mReader->getNodeName(), "material") != 0) - ThrowException( "Expected end of element."); + else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { + if (strcmp(mReader->getNodeName(), "material") != 0) + ThrowException("Expected end of element."); break; } @@ -1285,11 +1367,11 @@ void ColladaParser::ReadMaterial( Collada::Material& pMaterial) // ------------------------------------------------------------------------------------------------ // Reads a light entry into the given light -void ColladaParser::ReadLight( Collada::Light& pLight) +void ColladaParser::ReadLight(Collada::Light& pLight) { - while( mReader->read()) + while (mReader->read()) { - if( mReader->getNodeType() == irr::io::EXN_ELEMENT) { + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) { if (IsElement("light")) { SkipElement(); } @@ -1309,16 +1391,16 @@ void ColladaParser::ReadLight( Collada::Light& pLight) // text content contains 3 floats const char* content = GetTextContent(); - content = fast_atoreal_move( content, (ai_real&)pLight.mColor.r); - SkipSpacesAndLineEnd( &content); + content = fast_atoreal_move(content, (ai_real&)pLight.mColor.r); + SkipSpacesAndLineEnd(&content); - content = fast_atoreal_move( content, (ai_real&)pLight.mColor.g); - SkipSpacesAndLineEnd( &content); + content = fast_atoreal_move(content, (ai_real&)pLight.mColor.g); + SkipSpacesAndLineEnd(&content); - content = fast_atoreal_move( content, (ai_real&)pLight.mColor.b); - SkipSpacesAndLineEnd( &content); + content = fast_atoreal_move(content, (ai_real&)pLight.mColor.b); + SkipSpacesAndLineEnd(&content); - TestClosing( "color"); + TestClosing("color"); } else if (IsElement("constant_attenuation")) { pLight.mAttConstant = ReadFloatFromTextContent(); @@ -1370,8 +1452,8 @@ void ColladaParser::ReadLight( Collada::Light& pLight) TestClosing("decay_falloff"); } } - else if( mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { - if( strcmp( mReader->getNodeName(), "light") == 0) + else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { + if (strcmp(mReader->getNodeName(), "light") == 0) break; } } @@ -1379,11 +1461,11 @@ void ColladaParser::ReadLight( Collada::Light& pLight) // ------------------------------------------------------------------------------------------------ // Reads a camera entry into the given light -void ColladaParser::ReadCamera( Collada::Camera& pCamera) +void ColladaParser::ReadCamera(Collada::Camera& pCamera) { - while( mReader->read()) + while (mReader->read()) { - if( mReader->getNodeType() == irr::io::EXN_ELEMENT) { + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) { if (IsElement("camera")) { SkipElement(); } @@ -1411,8 +1493,8 @@ void ColladaParser::ReadCamera( Collada::Camera& pCamera) TestClosing("zfar"); } } - else if( mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { - if( strcmp( mReader->getNodeName(), "camera") == 0) + else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { + if (strcmp(mReader->getNodeName(), "camera") == 0) break; } } @@ -1426,28 +1508,29 @@ void ColladaParser::ReadEffectLibrary() return; } - while( mReader->read()) + while (mReader->read()) { - if( mReader->getNodeType() == irr::io::EXN_ELEMENT) { - if( IsElement( "effect")) + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) { + if (IsElement("effect")) { // read ID. Do I have to repeat my ranting about "optional" attributes? - int attrID = GetAttribute( "id"); - std::string id = mReader->getAttributeValue( attrID); + int attrID = GetAttribute("id"); + std::string id = mReader->getAttributeValue(attrID); // create an entry and store it in the library under its ID mEffectLibrary[id] = Effect(); // read on from there - ReadEffect( mEffectLibrary[id]); - } else + ReadEffect(mEffectLibrary[id]); + } + else { // ignore the rest SkipElement(); } } - else if( mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { - if( strcmp( mReader->getNodeName(), "library_effects") != 0) - ThrowException( "Expected end of element."); + else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { + if (strcmp(mReader->getNodeName(), "library_effects") != 0) + ThrowException("Expected end of element."); break; } @@ -1456,22 +1539,22 @@ void ColladaParser::ReadEffectLibrary() // ------------------------------------------------------------------------------------------------ // Reads an effect entry into the given effect -void ColladaParser::ReadEffect( Collada::Effect& pEffect) +void ColladaParser::ReadEffect(Collada::Effect& pEffect) { // for the moment we don't support any other type of effect. - while( mReader->read()) + while (mReader->read()) { - if( mReader->getNodeType() == irr::io::EXN_ELEMENT) + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) { - if( IsElement( "profile_COMMON")) - ReadEffectProfileCommon( pEffect); + if (IsElement("profile_COMMON")) + ReadEffectProfileCommon(pEffect); else SkipElement(); } - else if( mReader->getNodeType() == irr::io::EXN_ELEMENT_END) + else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { - if( strcmp( mReader->getNodeName(), "effect") != 0) - ThrowException( "Expected end of element."); + if (strcmp(mReader->getNodeName(), "effect") != 0) + ThrowException("Expected end of element."); break; } @@ -1480,107 +1563,107 @@ void ColladaParser::ReadEffect( Collada::Effect& pEffect) // ------------------------------------------------------------------------------------------------ // Reads an COMMON effect profile -void ColladaParser::ReadEffectProfileCommon( Collada::Effect& pEffect) +void ColladaParser::ReadEffectProfileCommon(Collada::Effect& pEffect) { - while( mReader->read()) + while (mReader->read()) { - if( mReader->getNodeType() == irr::io::EXN_ELEMENT) + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) { - if( IsElement( "newparam")) { + if (IsElement("newparam")) { // save ID - int attrSID = GetAttribute( "sid"); - std::string sid = mReader->getAttributeValue( attrSID); + int attrSID = GetAttribute("sid"); + std::string sid = mReader->getAttributeValue(attrSID); pEffect.mParams[sid] = EffectParam(); - ReadEffectParam( pEffect.mParams[sid]); + ReadEffectParam(pEffect.mParams[sid]); } - else if( IsElement( "technique") || IsElement( "extra")) + else if (IsElement("technique") || IsElement("extra")) { // just syntactic sugar } - else if( mFormat == FV_1_4_n && IsElement( "image")) + else if (mFormat == FV_1_4_n && IsElement("image")) { // read ID. Another entry which is "optional" by design but obligatory in reality - int attrID = GetAttribute( "id"); - std::string id = mReader->getAttributeValue( attrID); + int attrID = GetAttribute("id"); + std::string id = mReader->getAttributeValue(attrID); // create an entry and store it in the library under its ID mImageLibrary[id] = Image(); // read on from there - ReadImage( mImageLibrary[id]); + ReadImage(mImageLibrary[id]); } /* Shading modes */ - else if( IsElement( "phong")) + else if (IsElement("phong")) pEffect.mShadeType = Shade_Phong; - else if( IsElement( "constant")) + else if (IsElement("constant")) pEffect.mShadeType = Shade_Constant; - else if( IsElement( "lambert")) + else if (IsElement("lambert")) pEffect.mShadeType = Shade_Lambert; - else if( IsElement( "blinn")) + else if (IsElement("blinn")) pEffect.mShadeType = Shade_Blinn; /* Color + texture properties */ - else if( IsElement( "emission")) - ReadEffectColor( pEffect.mEmissive, pEffect.mTexEmissive); - else if( IsElement( "ambient")) - ReadEffectColor( pEffect.mAmbient, pEffect.mTexAmbient); - else if( IsElement( "diffuse")) - ReadEffectColor( pEffect.mDiffuse, pEffect.mTexDiffuse); - else if( IsElement( "specular")) - ReadEffectColor( pEffect.mSpecular, pEffect.mTexSpecular); - else if( IsElement( "reflective")) { - ReadEffectColor( pEffect.mReflective, pEffect.mTexReflective); + else if (IsElement("emission")) + ReadEffectColor(pEffect.mEmissive, pEffect.mTexEmissive); + else if (IsElement("ambient")) + ReadEffectColor(pEffect.mAmbient, pEffect.mTexAmbient); + else if (IsElement("diffuse")) + ReadEffectColor(pEffect.mDiffuse, pEffect.mTexDiffuse); + else if (IsElement("specular")) + ReadEffectColor(pEffect.mSpecular, pEffect.mTexSpecular); + else if (IsElement("reflective")) { + ReadEffectColor(pEffect.mReflective, pEffect.mTexReflective); } - else if( IsElement( "transparent")) { + else if (IsElement("transparent")) { pEffect.mHasTransparency = true; const char* opaque = mReader->getAttributeValueSafe("opaque"); - if(::strcmp(opaque, "RGB_ZERO") == 0 || ::strcmp(opaque, "RGB_ONE") == 0) { + if (::strcmp(opaque, "RGB_ZERO") == 0 || ::strcmp(opaque, "RGB_ONE") == 0) { pEffect.mRGBTransparency = true; } // In RGB_ZERO mode, the transparency is interpreted in reverse, go figure... - if(::strcmp(opaque, "RGB_ZERO") == 0 || ::strcmp(opaque, "A_ZERO") == 0) { - pEffect.mInvertTransparency = true; - } + if (::strcmp(opaque, "RGB_ZERO") == 0 || ::strcmp(opaque, "A_ZERO") == 0) { + pEffect.mInvertTransparency = true; + } - ReadEffectColor( pEffect.mTransparent,pEffect.mTexTransparent); + ReadEffectColor(pEffect.mTransparent, pEffect.mTexTransparent); } - else if( IsElement( "shininess")) - ReadEffectFloat( pEffect.mShininess); - else if( IsElement( "reflectivity")) - ReadEffectFloat( pEffect.mReflectivity); + else if (IsElement("shininess")) + ReadEffectFloat(pEffect.mShininess); + else if (IsElement("reflectivity")) + ReadEffectFloat(pEffect.mReflectivity); /* Single scalar properties */ - else if( IsElement( "transparency")) - ReadEffectFloat( pEffect.mTransparency); - else if( IsElement( "index_of_refraction")) - ReadEffectFloat( pEffect.mRefractIndex); + else if (IsElement("transparency")) + ReadEffectFloat(pEffect.mTransparency); + else if (IsElement("index_of_refraction")) + ReadEffectFloat(pEffect.mRefractIndex); // GOOGLEEARTH/OKINO extensions // ------------------------------------------------------- - else if( IsElement( "double_sided")) + else if (IsElement("double_sided")) pEffect.mDoubleSided = ReadBoolFromTextContent(); // FCOLLADA extensions // ------------------------------------------------------- - else if( IsElement( "bump")) { + else if (IsElement("bump")) { aiColor4D dummy; - ReadEffectColor( dummy,pEffect.mTexBump); + ReadEffectColor(dummy, pEffect.mTexBump); } // MAX3D extensions // ------------------------------------------------------- - else if( IsElement( "wireframe")) { + else if (IsElement("wireframe")) { pEffect.mWireframe = ReadBoolFromTextContent(); - TestClosing( "wireframe"); + TestClosing("wireframe"); } - else if( IsElement( "faceted")) { + else if (IsElement("faceted")) { pEffect.mFaceted = ReadBoolFromTextContent(); - TestClosing( "faceted"); + TestClosing("faceted"); } else { @@ -1588,8 +1671,8 @@ void ColladaParser::ReadEffectProfileCommon( Collada::Effect& pEffect) SkipElement(); } } - else if( mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { - if( strcmp( mReader->getNodeName(), "profile_COMMON") == 0) + else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { + if (strcmp(mReader->getNodeName(), "profile_COMMON") == 0) { break; } @@ -1599,92 +1682,92 @@ void ColladaParser::ReadEffectProfileCommon( Collada::Effect& pEffect) // ------------------------------------------------------------------------------------------------ // Read texture wrapping + UV transform settings from a profile==Maya chunk -void ColladaParser::ReadSamplerProperties( Sampler& out ) +void ColladaParser::ReadSamplerProperties(Sampler& out) { if (mReader->isEmptyElement()) { return; } - while( mReader->read()) + while (mReader->read()) { - if( mReader->getNodeType() == irr::io::EXN_ELEMENT) { + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) { // MAYA extensions // ------------------------------------------------------- - if( IsElement( "wrapU")) { + if (IsElement("wrapU")) { out.mWrapU = ReadBoolFromTextContent(); - TestClosing( "wrapU"); + TestClosing("wrapU"); } - else if( IsElement( "wrapV")) { + else if (IsElement("wrapV")) { out.mWrapV = ReadBoolFromTextContent(); - TestClosing( "wrapV"); + TestClosing("wrapV"); } - else if( IsElement( "mirrorU")) { + else if (IsElement("mirrorU")) { out.mMirrorU = ReadBoolFromTextContent(); - TestClosing( "mirrorU"); + TestClosing("mirrorU"); } - else if( IsElement( "mirrorV")) { + else if (IsElement("mirrorV")) { out.mMirrorV = ReadBoolFromTextContent(); - TestClosing( "mirrorV"); + TestClosing("mirrorV"); } - else if( IsElement( "repeatU")) { + else if (IsElement("repeatU")) { out.mTransform.mScaling.x = ReadFloatFromTextContent(); - TestClosing( "repeatU"); + TestClosing("repeatU"); } - else if( IsElement( "repeatV")) { + else if (IsElement("repeatV")) { out.mTransform.mScaling.y = ReadFloatFromTextContent(); - TestClosing( "repeatV"); + TestClosing("repeatV"); } - else if( IsElement( "offsetU")) { + else if (IsElement("offsetU")) { out.mTransform.mTranslation.x = ReadFloatFromTextContent(); - TestClosing( "offsetU"); + TestClosing("offsetU"); } - else if( IsElement( "offsetV")) { + else if (IsElement("offsetV")) { out.mTransform.mTranslation.y = ReadFloatFromTextContent(); - TestClosing( "offsetV"); + TestClosing("offsetV"); } - else if( IsElement( "rotateUV")) { + else if (IsElement("rotateUV")) { out.mTransform.mRotation = ReadFloatFromTextContent(); - TestClosing( "rotateUV"); + TestClosing("rotateUV"); } - else if( IsElement( "blend_mode")) { + else if (IsElement("blend_mode")) { const char* sz = GetTextContent(); // http://www.feelingsoftware.com/content/view/55/72/lang,en/ // NONE, OVER, IN, OUT, ADD, SUBTRACT, MULTIPLY, DIFFERENCE, LIGHTEN, DARKEN, SATURATE, DESATURATE and ILLUMINATE - if (0 == ASSIMP_strincmp(sz,"ADD",3)) + if (0 == ASSIMP_strincmp(sz, "ADD", 3)) out.mOp = aiTextureOp_Add; - else if (0 == ASSIMP_strincmp(sz,"SUBTRACT",8)) + else if (0 == ASSIMP_strincmp(sz, "SUBTRACT", 8)) out.mOp = aiTextureOp_Subtract; - else if (0 == ASSIMP_strincmp(sz,"MULTIPLY",8)) + else if (0 == ASSIMP_strincmp(sz, "MULTIPLY", 8)) out.mOp = aiTextureOp_Multiply; - else { + else { ASSIMP_LOG_WARN("Collada: Unsupported MAYA texture blend mode"); } - TestClosing( "blend_mode"); + TestClosing("blend_mode"); } // OKINO extensions // ------------------------------------------------------- - else if( IsElement( "weighting")) { + else if (IsElement("weighting")) { out.mWeighting = ReadFloatFromTextContent(); - TestClosing( "weighting"); + TestClosing("weighting"); } - else if( IsElement( "mix_with_previous_layer")) { + else if (IsElement("mix_with_previous_layer")) { out.mMixWithPrevious = ReadFloatFromTextContent(); - TestClosing( "mix_with_previous_layer"); + TestClosing("mix_with_previous_layer"); } // MAX3D extensions // ------------------------------------------------------- - else if( IsElement( "amount")) { + else if (IsElement("amount")) { out.mWeighting = ReadFloatFromTextContent(); - TestClosing( "amount"); + TestClosing("amount"); } } - else if( mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { - if( strcmp( mReader->getNodeName(), "technique") == 0) + else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { + if (strcmp(mReader->getNodeName(), "technique") == 0) break; } } @@ -1692,7 +1775,7 @@ void ColladaParser::ReadSamplerProperties( Sampler& out ) // ------------------------------------------------------------------------------------------------ // Reads an effect entry containing a color or a texture defining that color -void ColladaParser::ReadEffectColor( aiColor4D& pColor, Sampler& pSampler) +void ColladaParser::ReadEffectColor(aiColor4D& pColor, Sampler& pSampler) { if (mReader->isEmptyElement()) return; @@ -1700,64 +1783,64 @@ void ColladaParser::ReadEffectColor( aiColor4D& pColor, Sampler& pSampler) // Save current element name const std::string curElem = mReader->getNodeName(); - while( mReader->read()) + while (mReader->read()) { - if( mReader->getNodeType() == irr::io::EXN_ELEMENT) { - if( IsElement( "color")) + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) { + if (IsElement("color")) { // text content contains 4 floats const char* content = GetTextContent(); - content = fast_atoreal_move( content, (ai_real&)pColor.r); - SkipSpacesAndLineEnd( &content); + content = fast_atoreal_move(content, (ai_real&)pColor.r); + SkipSpacesAndLineEnd(&content); - content = fast_atoreal_move( content, (ai_real&)pColor.g); - SkipSpacesAndLineEnd( &content); + content = fast_atoreal_move(content, (ai_real&)pColor.g); + SkipSpacesAndLineEnd(&content); - content = fast_atoreal_move( content, (ai_real&)pColor.b); - SkipSpacesAndLineEnd( &content); + content = fast_atoreal_move(content, (ai_real&)pColor.b); + SkipSpacesAndLineEnd(&content); - content = fast_atoreal_move( content, (ai_real&)pColor.a); - SkipSpacesAndLineEnd( &content); - TestClosing( "color"); + content = fast_atoreal_move(content, (ai_real&)pColor.a); + SkipSpacesAndLineEnd(&content); + TestClosing("color"); } - else if( IsElement( "texture")) + else if (IsElement("texture")) { // get name of source texture/sampler - int attrTex = GetAttribute( "texture"); - pSampler.mName = mReader->getAttributeValue( attrTex); + int attrTex = GetAttribute("texture"); + pSampler.mName = mReader->getAttributeValue(attrTex); // get name of UV source channel. Specification demands it to be there, but some exporters // don't write it. It will be the default UV channel in case it's missing. - attrTex = TestAttribute( "texcoord"); - if( attrTex >= 0 ) - pSampler.mUVChannel = mReader->getAttributeValue( attrTex); + attrTex = TestAttribute("texcoord"); + if (attrTex >= 0) + pSampler.mUVChannel = mReader->getAttributeValue(attrTex); //SkipElement(); // as we've read texture, the color needs to be 1,1,1,1 pColor = aiColor4D(1.f, 1.f, 1.f, 1.f); } - else if( IsElement( "technique")) + else if (IsElement("technique")) { - const int _profile = GetAttribute( "profile"); - const char* profile = mReader->getAttributeValue( _profile ); + const int _profile = GetAttribute("profile"); + const char* profile = mReader->getAttributeValue(_profile); // Some extensions are quite useful ... ReadSamplerProperties processes // several extensions in MAYA, OKINO and MAX3D profiles. - if (!::strcmp(profile,"MAYA") || !::strcmp(profile,"MAX3D") || !::strcmp(profile,"OKINO")) + if (!::strcmp(profile, "MAYA") || !::strcmp(profile, "MAX3D") || !::strcmp(profile, "OKINO")) { // get more information on this sampler ReadSamplerProperties(pSampler); } else SkipElement(); } - else if( !IsElement( "extra")) + else if (!IsElement("extra")) { // ignore the rest SkipElement(); } } - else if( mReader->getNodeType() == irr::io::EXN_ELEMENT_END){ + else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { if (mReader->getNodeName() == curElem) break; } @@ -1766,26 +1849,27 @@ void ColladaParser::ReadEffectColor( aiColor4D& pColor, Sampler& pSampler) // ------------------------------------------------------------------------------------------------ // Reads an effect entry containing a float -void ColladaParser::ReadEffectFloat( ai_real& pFloat) +void ColladaParser::ReadEffectFloat(ai_real& pFloat) { - while( mReader->read()) + while (mReader->read()) { - if( mReader->getNodeType() == irr::io::EXN_ELEMENT){ - if( IsElement( "float")) + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) { + if (IsElement("float")) { // text content contains a single floats const char* content = GetTextContent(); - content = fast_atoreal_move( content, pFloat); - SkipSpacesAndLineEnd( &content); + content = fast_atoreal_move(content, pFloat); + SkipSpacesAndLineEnd(&content); - TestClosing( "float"); - } else + TestClosing("float"); + } + else { // ignore the rest SkipElement(); } } - else if( mReader->getNodeType() == irr::io::EXN_ELEMENT_END){ + else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { break; } } @@ -1793,54 +1877,55 @@ void ColladaParser::ReadEffectFloat( ai_real& pFloat) // ------------------------------------------------------------------------------------------------ // Reads an effect parameter specification of any kind -void ColladaParser::ReadEffectParam( Collada::EffectParam& pParam) +void ColladaParser::ReadEffectParam(Collada::EffectParam& pParam) { - while( mReader->read()) + while (mReader->read()) { - if( mReader->getNodeType() == irr::io::EXN_ELEMENT) { - if( IsElement( "surface")) + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) { + if (IsElement("surface")) { // image ID given inside tags - TestOpening( "init_from"); + TestOpening("init_from"); const char* content = GetTextContent(); pParam.mType = Param_Surface; pParam.mReference = content; - TestClosing( "init_from"); + TestClosing("init_from"); // don't care for remaining stuff - SkipElement( "surface"); + SkipElement("surface"); } - else if( IsElement( "sampler2D") && (FV_1_4_n == mFormat || FV_1_3_n == mFormat)) + else if (IsElement("sampler2D") && (FV_1_4_n == mFormat || FV_1_3_n == mFormat)) { // surface ID is given inside tags - TestOpening( "source"); + TestOpening("source"); const char* content = GetTextContent(); pParam.mType = Param_Sampler; pParam.mReference = content; - TestClosing( "source"); + TestClosing("source"); // don't care for remaining stuff - SkipElement( "sampler2D"); + SkipElement("sampler2D"); } - else if( IsElement( "sampler2D")) + else if (IsElement("sampler2D")) { // surface ID is given inside tags - TestOpening( "instance_image"); + TestOpening("instance_image"); int attrURL = GetAttribute("url"); - const char* url = mReader->getAttributeValue( attrURL); - if( url[0] != '#') - ThrowException( "Unsupported URL format in instance_image"); + const char* url = mReader->getAttributeValue(attrURL); + if (url[0] != '#') + ThrowException("Unsupported URL format in instance_image"); url++; pParam.mType = Param_Sampler; pParam.mReference = url; - SkipElement( "sampler2D"); - } else + SkipElement("sampler2D"); + } + else { // ignore unknown element SkipElement(); } } - else if( mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { + else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { break; } } @@ -1850,18 +1935,18 @@ void ColladaParser::ReadEffectParam( Collada::EffectParam& pParam) // Reads the geometry library contents void ColladaParser::ReadGeometryLibrary() { - if( mReader->isEmptyElement()) + if (mReader->isEmptyElement()) return; - while( mReader->read()) + while (mReader->read()) { - if( mReader->getNodeType() == irr::io::EXN_ELEMENT) + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) { - if( IsElement( "geometry")) + if (IsElement("geometry")) { // read ID. Another entry which is "optional" by design but obligatory in reality - int indexID = GetAttribute( "id"); - std::string id = mReader->getAttributeValue( indexID); + int indexID = GetAttribute("id"); + std::string id = mReader->getAttributeValue(indexID); // TODO: (thom) support SIDs // ai_assert( TestAttribute( "sid") == -1); @@ -1872,23 +1957,24 @@ void ColladaParser::ReadGeometryLibrary() // read the mesh name if it exists const int nameIndex = TestAttribute("name"); - if(nameIndex != -1) + if (nameIndex != -1) { mesh->mName = mReader->getAttributeValue(nameIndex); } // read on from there - ReadGeometry( mesh); - } else + ReadGeometry(mesh); + } + else { // ignore the rest SkipElement(); } } - else if( mReader->getNodeType() == irr::io::EXN_ELEMENT_END) + else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { - if( strcmp( mReader->getNodeName(), "library_geometries") != 0) - ThrowException( "Expected end of element."); + if (strcmp(mReader->getNodeName(), "library_geometries") != 0) + ThrowException("Expected end of element."); break; } @@ -1897,29 +1983,30 @@ void ColladaParser::ReadGeometryLibrary() // ------------------------------------------------------------------------------------------------ // Reads a geometry from the geometry library. -void ColladaParser::ReadGeometry( Collada::Mesh* pMesh) +void ColladaParser::ReadGeometry(Collada::Mesh* pMesh) { - if( mReader->isEmptyElement()) + if (mReader->isEmptyElement()) return; - while( mReader->read()) + while (mReader->read()) { - if( mReader->getNodeType() == irr::io::EXN_ELEMENT) + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) { - if( IsElement( "mesh")) + if (IsElement("mesh")) { // read on from there - ReadMesh( pMesh); - } else + ReadMesh(pMesh); + } + else { // ignore the rest SkipElement(); } } - else if( mReader->getNodeType() == irr::io::EXN_ELEMENT_END) + else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { - if( strcmp( mReader->getNodeName(), "geometry") != 0) - ThrowException( "Expected end of element."); + if (strcmp(mReader->getNodeName(), "geometry") != 0) + ThrowException("Expected end of element."); break; } @@ -1928,50 +2015,52 @@ void ColladaParser::ReadGeometry( Collada::Mesh* pMesh) // ------------------------------------------------------------------------------------------------ // Reads a mesh from the geometry library -void ColladaParser::ReadMesh( Mesh* pMesh) +void ColladaParser::ReadMesh(Mesh* pMesh) { - if( mReader->isEmptyElement()) + if (mReader->isEmptyElement()) return; - while( mReader->read()) + while (mReader->read()) { - if( mReader->getNodeType() == irr::io::EXN_ELEMENT) + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) { - if( IsElement( "source")) + if (IsElement("source")) { // we have professionals dealing with this ReadSource(); } - else if( IsElement( "vertices")) + else if (IsElement("vertices")) { // read per-vertex mesh data - ReadVertexData( pMesh); + ReadVertexData(pMesh); } - else if( IsElement( "triangles") || IsElement( "lines") || IsElement( "linestrips") - || IsElement( "polygons") || IsElement( "polylist") || IsElement( "trifans") || IsElement( "tristrips")) + else if (IsElement("triangles") || IsElement("lines") || IsElement("linestrips") + || IsElement("polygons") || IsElement("polylist") || IsElement("trifans") || IsElement("tristrips")) { // read per-index mesh data and faces setup - ReadIndexData( pMesh); - } else + ReadIndexData(pMesh); + } + else { // ignore the restf SkipElement(); } } - else if( mReader->getNodeType() == irr::io::EXN_ELEMENT_END) + else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { - if( strcmp( mReader->getNodeName(), "technique_common") == 0) + if (strcmp(mReader->getNodeName(), "technique_common") == 0) { // end of another meaningless element - read over it } - else if( strcmp( mReader->getNodeName(), "mesh") == 0) + else if (strcmp(mReader->getNodeName(), "mesh") == 0) { // end of element - we're done here break; - } else + } + else { // everything else should be punished - ThrowException( "Expected end of element."); + ThrowException("Expected end of element."); } } } @@ -1981,44 +2070,46 @@ void ColladaParser::ReadMesh( Mesh* pMesh) // Reads a source element void ColladaParser::ReadSource() { - int indexID = GetAttribute( "id"); - std::string sourceID = mReader->getAttributeValue( indexID); + int indexID = GetAttribute("id"); + std::string sourceID = mReader->getAttributeValue(indexID); - while( mReader->read()) + while (mReader->read()) { - if( mReader->getNodeType() == irr::io::EXN_ELEMENT) + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) { - if( IsElement( "float_array") || IsElement( "IDREF_array") || IsElement( "Name_array")) + if (IsElement("float_array") || IsElement("IDREF_array") || IsElement("Name_array")) { ReadDataArray(); } - else if( IsElement( "technique_common")) + else if (IsElement("technique_common")) { // I don't care for your profiles } - else if( IsElement( "accessor")) + else if (IsElement("accessor")) { - ReadAccessor( sourceID); - } else + ReadAccessor(sourceID); + } + else { // ignore the rest SkipElement(); } } - else if( mReader->getNodeType() == irr::io::EXN_ELEMENT_END) + else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { - if( strcmp( mReader->getNodeName(), "source") == 0) + if (strcmp(mReader->getNodeName(), "source") == 0) { // end of - we're done break; } - else if( strcmp( mReader->getNodeName(), "technique_common") == 0) + else if (strcmp(mReader->getNodeName(), "technique_common") == 0) { // end of another meaningless element - read over it - } else + } + else { // everything else should be punished - ThrowException( "Expected end of element."); + ThrowException("Expected end of element."); } } } @@ -2030,83 +2121,84 @@ void ColladaParser::ReadDataArray() { std::string elmName = mReader->getNodeName(); bool isStringArray = (elmName == "IDREF_array" || elmName == "Name_array"); - bool isEmptyElement = mReader->isEmptyElement(); + bool isEmptyElement = mReader->isEmptyElement(); // read attributes - int indexID = GetAttribute( "id"); - std::string id = mReader->getAttributeValue( indexID); - int indexCount = GetAttribute( "count"); - unsigned int count = (unsigned int) mReader->getAttributeValueAsInt( indexCount); + int indexID = GetAttribute("id"); + std::string id = mReader->getAttributeValue(indexID); + int indexCount = GetAttribute("count"); + unsigned int count = (unsigned int)mReader->getAttributeValueAsInt(indexCount); const char* content = TestTextContent(); - // read values and store inside an array in the data library - mDataLibrary[id] = Data(); - Data& data = mDataLibrary[id]; - data.mIsStringArray = isStringArray; + // read values and store inside an array in the data library + mDataLibrary[id] = Data(); + Data& data = mDataLibrary[id]; + data.mIsStringArray = isStringArray; - // some exporters write empty data arrays, but we need to conserve them anyways because others might reference them - if (content) - { - if( isStringArray) + // some exporters write empty data arrays, but we need to conserve them anyways because others might reference them + if (content) + { + if (isStringArray) { - data.mStrings.reserve( count); + data.mStrings.reserve(count); std::string s; - for( unsigned int a = 0; a < count; a++) + for (unsigned int a = 0; a < count; a++) { - if( *content == 0) - ThrowException( "Expected more values while reading IDREF_array contents."); + if (*content == 0) + ThrowException("Expected more values while reading IDREF_array contents."); s.clear(); - while( !IsSpaceOrNewLine( *content)) + while (!IsSpaceOrNewLine(*content)) s += *content++; - data.mStrings.push_back( s); + data.mStrings.push_back(s); - SkipSpacesAndLineEnd( &content); + SkipSpacesAndLineEnd(&content); } - } else + } + else { - data.mValues.reserve( count); + data.mValues.reserve(count); - for( unsigned int a = 0; a < count; a++) + for (unsigned int a = 0; a < count; a++) { - if( *content == 0) - ThrowException( "Expected more values while reading float_array contents."); + if (*content == 0) + ThrowException("Expected more values while reading float_array contents."); ai_real value; // read a number - content = fast_atoreal_move( content, value); - data.mValues.push_back( value); + content = fast_atoreal_move(content, value); + data.mValues.push_back(value); // skip whitespace after it - SkipSpacesAndLineEnd( &content); + SkipSpacesAndLineEnd(&content); } } } - // test for closing tag - if( !isEmptyElement ) - TestClosing( elmName.c_str()); + // test for closing tag + if (!isEmptyElement) + TestClosing(elmName.c_str()); } // ------------------------------------------------------------------------------------------------ // Reads an accessor and stores it in the global library -void ColladaParser::ReadAccessor( const std::string& pID) +void ColladaParser::ReadAccessor(const std::string& pID) { // read accessor attributes - int attrSource = GetAttribute( "source"); - const char* source = mReader->getAttributeValue( attrSource); - if( source[0] != '#') - ThrowException( format() << "Unknown reference format in url \"" << source << "\" in source attribute of element." ); - int attrCount = GetAttribute( "count"); - unsigned int count = (unsigned int) mReader->getAttributeValueAsInt( attrCount); - int attrOffset = TestAttribute( "offset"); + int attrSource = GetAttribute("source"); + const char* source = mReader->getAttributeValue(attrSource); + if (source[0] != '#') + ThrowException(format() << "Unknown reference format in url \"" << source << "\" in source attribute of element."); + int attrCount = GetAttribute("count"); + unsigned int count = (unsigned int)mReader->getAttributeValueAsInt(attrCount); + int attrOffset = TestAttribute("offset"); unsigned int offset = 0; - if( attrOffset > -1) - offset = (unsigned int) mReader->getAttributeValueAsInt( attrOffset); - int attrStride = TestAttribute( "stride"); + if (attrOffset > -1) + offset = (unsigned int)mReader->getAttributeValueAsInt(attrOffset); + int attrStride = TestAttribute("stride"); unsigned int stride = 1; - if( attrStride > -1) - stride = (unsigned int) mReader->getAttributeValueAsInt( attrStride); + if (attrStride > -1) + stride = (unsigned int)mReader->getAttributeValueAsInt(attrStride); // store in the library under the given ID mAccessorLibrary[pID] = Accessor(); @@ -2114,77 +2206,78 @@ void ColladaParser::ReadAccessor( const std::string& pID) acc.mCount = count; acc.mOffset = offset; acc.mStride = stride; - acc.mSource = source+1; // ignore the leading '#' + acc.mSource = source + 1; // ignore the leading '#' acc.mSize = 0; // gets incremented with every param // and read the components - while( mReader->read()) + while (mReader->read()) { - if( mReader->getNodeType() == irr::io::EXN_ELEMENT) + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) { - if( IsElement( "param")) + if (IsElement("param")) { // read data param - int attrName = TestAttribute( "name"); + int attrName = TestAttribute("name"); std::string name; - if( attrName > -1) + if (attrName > -1) { - name = mReader->getAttributeValue( attrName); + name = mReader->getAttributeValue(attrName); // analyse for common type components and store it's sub-offset in the corresponding field /* Cartesian coordinates */ - if( name == "X") acc.mSubOffset[0] = acc.mParams.size(); - else if( name == "Y") acc.mSubOffset[1] = acc.mParams.size(); - else if( name == "Z") acc.mSubOffset[2] = acc.mParams.size(); + if (name == "X") acc.mSubOffset[0] = acc.mParams.size(); + else if (name == "Y") acc.mSubOffset[1] = acc.mParams.size(); + else if (name == "Z") acc.mSubOffset[2] = acc.mParams.size(); /* RGBA colors */ - else if( name == "R") acc.mSubOffset[0] = acc.mParams.size(); - else if( name == "G") acc.mSubOffset[1] = acc.mParams.size(); - else if( name == "B") acc.mSubOffset[2] = acc.mParams.size(); - else if( name == "A") acc.mSubOffset[3] = acc.mParams.size(); + else if (name == "R") acc.mSubOffset[0] = acc.mParams.size(); + else if (name == "G") acc.mSubOffset[1] = acc.mParams.size(); + else if (name == "B") acc.mSubOffset[2] = acc.mParams.size(); + else if (name == "A") acc.mSubOffset[3] = acc.mParams.size(); /* UVWQ (STPQ) texture coordinates */ - else if( name == "S") acc.mSubOffset[0] = acc.mParams.size(); - else if( name == "T") acc.mSubOffset[1] = acc.mParams.size(); - else if( name == "P") acc.mSubOffset[2] = acc.mParams.size(); - // else if( name == "Q") acc.mSubOffset[3] = acc.mParams.size(); - /* 4D uv coordinates are not supported in Assimp */ + else if (name == "S") acc.mSubOffset[0] = acc.mParams.size(); + else if (name == "T") acc.mSubOffset[1] = acc.mParams.size(); + else if (name == "P") acc.mSubOffset[2] = acc.mParams.size(); + // else if( name == "Q") acc.mSubOffset[3] = acc.mParams.size(); + /* 4D uv coordinates are not supported in Assimp */ - /* Generic extra data, interpreted as UV data, too*/ - else if( name == "U") acc.mSubOffset[0] = acc.mParams.size(); - else if( name == "V") acc.mSubOffset[1] = acc.mParams.size(); + /* Generic extra data, interpreted as UV data, too*/ + else if (name == "U") acc.mSubOffset[0] = acc.mParams.size(); + else if (name == "V") acc.mSubOffset[1] = acc.mParams.size(); //else // DefaultLogger::get()->warn( format() << "Unknown accessor parameter \"" << name << "\". Ignoring data channel." ); } // read data type - int attrType = TestAttribute( "type"); - if( attrType > -1) + int attrType = TestAttribute("type"); + if (attrType > -1) { // for the moment we only distinguish between a 4x4 matrix and anything else. // TODO: (thom) I don't have a spec here at work. Check if there are other multi-value types // which should be tested for here. - std::string type = mReader->getAttributeValue( attrType); - if( type == "float4x4") + std::string type = mReader->getAttributeValue(attrType); + if (type == "float4x4") acc.mSize += 16; else acc.mSize += 1; } - acc.mParams.push_back( name); + acc.mParams.push_back(name); // skip remaining stuff of this element, if any SkipElement(); - } else + } + else { - ThrowException( format() << "Unexpected sub element <" << mReader->getNodeName() << "> in tag " ); + ThrowException(format() << "Unexpected sub element <" << mReader->getNodeName() << "> in tag "); } } - else if( mReader->getNodeType() == irr::io::EXN_ELEMENT_END) + else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { - if( strcmp( mReader->getNodeName(), "accessor") != 0) - ThrowException( "Expected end of element."); + if (strcmp(mReader->getNodeName(), "accessor") != 0) + ThrowException("Expected end of element."); break; } } @@ -2192,29 +2285,30 @@ void ColladaParser::ReadAccessor( const std::string& pID) // ------------------------------------------------------------------------------------------------ // Reads input declarations of per-vertex mesh data into the given mesh -void ColladaParser::ReadVertexData( Mesh* pMesh) +void ColladaParser::ReadVertexData(Mesh* pMesh) { // extract the ID of the element. Not that we care, but to catch strange referencing schemes we should warn about - int attrID= GetAttribute( "id"); - pMesh->mVertexID = mReader->getAttributeValue( attrID); + int attrID = GetAttribute("id"); + pMesh->mVertexID = mReader->getAttributeValue(attrID); // a number of elements - while( mReader->read()) + while (mReader->read()) { - if( mReader->getNodeType() == irr::io::EXN_ELEMENT) + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) { - if( IsElement( "input")) + if (IsElement("input")) { - ReadInputChannel( pMesh->mPerVertexData); - } else + ReadInputChannel(pMesh->mPerVertexData); + } + else { - ThrowException( format() << "Unexpected sub element <" << mReader->getNodeName() << "> in tag " ); + ThrowException(format() << "Unexpected sub element <" << mReader->getNodeName() << "> in tag "); } } - else if( mReader->getNodeType() == irr::io::EXN_ELEMENT_END) + else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { - if( strcmp( mReader->getNodeName(), "vertices") != 0) - ThrowException( "Expected end of element."); + if (strcmp(mReader->getNodeName(), "vertices") != 0) + ThrowException("Expected end of element."); break; } @@ -2223,79 +2317,79 @@ void ColladaParser::ReadVertexData( Mesh* pMesh) // ------------------------------------------------------------------------------------------------ // Reads input declarations of per-index mesh data into the given mesh -void ColladaParser::ReadIndexData( Mesh* pMesh) +void ColladaParser::ReadIndexData(Mesh* pMesh) { std::vector vcount; std::vector perIndexData; // read primitive count from the attribute - int attrCount = GetAttribute( "count"); - size_t numPrimitives = (size_t) mReader->getAttributeValueAsInt( attrCount); + int attrCount = GetAttribute("count"); + size_t numPrimitives = (size_t)mReader->getAttributeValueAsInt(attrCount); // some mesh types (e.g. tristrips) don't specify primitive count upfront, // so we need to sum up the actual number of primitives while we read the

-tags size_t actualPrimitives = 0; // material subgroup - int attrMaterial = TestAttribute( "material"); + int attrMaterial = TestAttribute("material"); SubMesh subgroup; - if( attrMaterial > -1) - subgroup.mMaterial = mReader->getAttributeValue( attrMaterial); + if (attrMaterial > -1) + subgroup.mMaterial = mReader->getAttributeValue(attrMaterial); // distinguish between polys and triangles std::string elementName = mReader->getNodeName(); PrimitiveType primType = Prim_Invalid; - if( IsElement( "lines")) + if (IsElement("lines")) primType = Prim_Lines; - else if( IsElement( "linestrips")) + else if (IsElement("linestrips")) primType = Prim_LineStrip; - else if( IsElement( "polygons")) + else if (IsElement("polygons")) primType = Prim_Polygon; - else if( IsElement( "polylist")) + else if (IsElement("polylist")) primType = Prim_Polylist; - else if( IsElement( "triangles")) + else if (IsElement("triangles")) primType = Prim_Triangles; - else if( IsElement( "trifans")) + else if (IsElement("trifans")) primType = Prim_TriFans; - else if( IsElement( "tristrips")) + else if (IsElement("tristrips")) primType = Prim_TriStrips; - ai_assert( primType != Prim_Invalid); + ai_assert(primType != Prim_Invalid); // also a number of elements, but in addition a

primitive collection and probably index counts for all primitives - while( mReader->read()) + while (mReader->read()) { - if( mReader->getNodeType() == irr::io::EXN_ELEMENT) + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) { - if( IsElement( "input")) + if (IsElement("input")) { - ReadInputChannel( perIndexData); + ReadInputChannel(perIndexData); } - else if( IsElement( "vcount")) + else if (IsElement("vcount")) { - if( !mReader->isEmptyElement()) + if (!mReader->isEmptyElement()) { if (numPrimitives) // It is possible to define a mesh without any primitives { // case - specifies the number of indices for each polygon const char* content = GetTextContent(); - vcount.reserve( numPrimitives); - for( unsigned int a = 0; a < numPrimitives; a++) + vcount.reserve(numPrimitives); + for (unsigned int a = 0; a < numPrimitives; a++) { - if( *content == 0) - ThrowException( "Expected more values while reading contents."); + if (*content == 0) + ThrowException("Expected more values while reading contents."); // read a number - vcount.push_back( (size_t) strtoul10( content, &content)); + vcount.push_back((size_t)strtoul10(content, &content)); // skip whitespace after it - SkipSpacesAndLineEnd( &content); + SkipSpacesAndLineEnd(&content); } } - TestClosing( "vcount"); + TestClosing("vcount"); } } - else if( IsElement( "p")) + else if (IsElement("p")) { - if( !mReader->isEmptyElement()) + if (!mReader->isEmptyElement()) { // now here the actual fun starts - these are the indices to construct the mesh data from actualPrimitives += ReadPrimitives(pMesh, perIndexData, numPrimitives, vcount, primType); @@ -2304,23 +2398,25 @@ void ColladaParser::ReadIndexData( Mesh* pMesh) else if (IsElement("extra")) { SkipElement("extra"); - } else if ( IsElement("ph")) { + } + else if (IsElement("ph")) { SkipElement("ph"); - } else { - ThrowException( format() << "Unexpected sub element <" << mReader->getNodeName() << "> in tag <" << elementName << ">" ); + } + else { + ThrowException(format() << "Unexpected sub element <" << mReader->getNodeName() << "> in tag <" << elementName << ">"); } } - else if( mReader->getNodeType() == irr::io::EXN_ELEMENT_END) + else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { - if( mReader->getNodeName() != elementName) - ThrowException( format() << "Expected end of <" << elementName << "> element." ); + if (mReader->getNodeName() != elementName) + ThrowException(format() << "Expected end of <" << elementName << "> element."); break; } } #ifdef ASSIMP_BUILD_DEBUG - if (primType != Prim_TriFans && primType != Prim_TriStrips && primType != Prim_LineStrip && + if (primType != Prim_TriFans && primType != Prim_TriStrips && primType != Prim_LineStrip && primType != Prim_Lines) { // this is ONLY to workaround a bug in SketchUp 15.3.331 where it writes the wrong 'count' when it writes out the 'lines'. ai_assert(actualPrimitives == numPrimitives); } @@ -2333,42 +2429,42 @@ void ColladaParser::ReadIndexData( Mesh* pMesh) // ------------------------------------------------------------------------------------------------ // Reads a single input channel element and stores it in the given array, if valid -void ColladaParser::ReadInputChannel( std::vector& poChannels) +void ColladaParser::ReadInputChannel(std::vector& poChannels) { InputChannel channel; // read semantic - int attrSemantic = GetAttribute( "semantic"); - std::string semantic = mReader->getAttributeValue( attrSemantic); - channel.mType = GetTypeForSemantic( semantic); + int attrSemantic = GetAttribute("semantic"); + std::string semantic = mReader->getAttributeValue(attrSemantic); + channel.mType = GetTypeForSemantic(semantic); // read source - int attrSource = GetAttribute( "source"); - const char* source = mReader->getAttributeValue( attrSource); - if( source[0] != '#') - ThrowException( format() << "Unknown reference format in url \"" << source << "\" in source attribute of element." ); - channel.mAccessor = source+1; // skipping the leading #, hopefully the remaining text is the accessor ID only + int attrSource = GetAttribute("source"); + const char* source = mReader->getAttributeValue(attrSource); + if (source[0] != '#') + ThrowException(format() << "Unknown reference format in url \"" << source << "\" in source attribute of element."); + channel.mAccessor = source + 1; // skipping the leading #, hopefully the remaining text is the accessor ID only // read index offset, if per-index - int attrOffset = TestAttribute( "offset"); - if( attrOffset > -1) - channel.mOffset = mReader->getAttributeValueAsInt( attrOffset); + int attrOffset = TestAttribute("offset"); + if (attrOffset > -1) + channel.mOffset = mReader->getAttributeValueAsInt(attrOffset); // read set if texture coordinates - if(channel.mType == IT_Texcoord || channel.mType == IT_Color){ + if (channel.mType == IT_Texcoord || channel.mType == IT_Color) { int attrSet = TestAttribute("set"); - if(attrSet > -1){ - attrSet = mReader->getAttributeValueAsInt( attrSet); - if(attrSet < 0) - ThrowException( format() << "Invalid index \"" << (attrSet) << "\" in set attribute of element" ); + if (attrSet > -1) { + attrSet = mReader->getAttributeValueAsInt(attrSet); + if (attrSet < 0) + ThrowException(format() << "Invalid index \"" << (attrSet) << "\" in set attribute of element"); channel.mIndex = attrSet; } } // store, if valid type - if( channel.mType != IT_Invalid) - poChannels.push_back( channel); + if (channel.mType != IT_Invalid) + poChannels.push_back(channel); // skip remaining stuff of this element, if any SkipElement(); @@ -2376,116 +2472,118 @@ void ColladaParser::ReadInputChannel( std::vector& poChannels) // ------------------------------------------------------------------------------------------------ // Reads a

primitive index list and assembles the mesh data into the given mesh -size_t ColladaParser::ReadPrimitives( Mesh* pMesh, std::vector& pPerIndexChannels, +size_t ColladaParser::ReadPrimitives(Mesh* pMesh, std::vector& pPerIndexChannels, size_t pNumPrimitives, const std::vector& pVCount, PrimitiveType pPrimType) { // determine number of indices coming per vertex // find the offset index for all per-vertex channels size_t numOffsets = 1; size_t perVertexOffset = SIZE_MAX; // invalid value - for( const InputChannel& channel : pPerIndexChannels) + for (const InputChannel& channel : pPerIndexChannels) { - numOffsets = std::max( numOffsets, channel.mOffset+1); - if( channel.mType == IT_Vertex) + numOffsets = std::max(numOffsets, channel.mOffset + 1); + if (channel.mType == IT_Vertex) perVertexOffset = channel.mOffset; } // determine the expected number of indices size_t expectedPointCount = 0; - switch( pPrimType) + switch (pPrimType) { - case Prim_Polylist: - { - for( size_t i : pVCount) - expectedPointCount += i; - break; - } - case Prim_Lines: - expectedPointCount = 2 * pNumPrimitives; - break; - case Prim_Triangles: - expectedPointCount = 3 * pNumPrimitives; - break; - default: - // other primitive types don't state the index count upfront... we need to guess - break; + case Prim_Polylist: + { + for (size_t i : pVCount) + expectedPointCount += i; + break; + } + case Prim_Lines: + expectedPointCount = 2 * pNumPrimitives; + break; + case Prim_Triangles: + expectedPointCount = 3 * pNumPrimitives; + break; + default: + // other primitive types don't state the index count upfront... we need to guess + break; } // and read all indices into a temporary array std::vector indices; - if( expectedPointCount > 0) - indices.reserve( expectedPointCount * numOffsets); + if (expectedPointCount > 0) + indices.reserve(expectedPointCount * numOffsets); if (pNumPrimitives > 0) // It is possible to not contain any indices { const char* content = GetTextContent(); - while( *content != 0) + while (*content != 0) { // read a value. // Hack: (thom) Some exporters put negative indices sometimes. We just try to carry on anyways. - int value = std::max( 0, strtol10( content, &content)); - indices.push_back( size_t( value)); + int value = std::max(0, strtol10(content, &content)); + indices.push_back(size_t(value)); // skip whitespace after it - SkipSpacesAndLineEnd( &content); + SkipSpacesAndLineEnd(&content); } } - // complain if the index count doesn't fit - if( expectedPointCount > 0 && indices.size() != expectedPointCount * numOffsets) { + // complain if the index count doesn't fit + if (expectedPointCount > 0 && indices.size() != expectedPointCount * numOffsets) { if (pPrimType == Prim_Lines) { // HACK: We just fix this number since SketchUp 15.3.331 writes the wrong 'count' for 'lines' - ReportWarning( "Expected different index count in

element, %zu instead of %zu.", indices.size(), expectedPointCount * numOffsets); + ReportWarning("Expected different index count in

element, %zu instead of %zu.", indices.size(), expectedPointCount * numOffsets); pNumPrimitives = (indices.size() / numOffsets) / 2; - } else - ThrowException( "Expected different index count in

element."); + } + else + ThrowException("Expected different index count in

element."); - } else if( expectedPointCount == 0 && (indices.size() % numOffsets) != 0) - ThrowException( "Expected different index count in

element."); + } + else if (expectedPointCount == 0 && (indices.size() % numOffsets) != 0) + ThrowException("Expected different index count in

element."); - // find the data for all sources - for( std::vector::iterator it = pMesh->mPerVertexData.begin(); it != pMesh->mPerVertexData.end(); ++it) + // find the data for all sources + for (std::vector::iterator it = pMesh->mPerVertexData.begin(); it != pMesh->mPerVertexData.end(); ++it) { InputChannel& input = *it; - if( input.mResolved) + if (input.mResolved) continue; // find accessor - input.mResolved = &ResolveLibraryReference( mAccessorLibrary, input.mAccessor); + input.mResolved = &ResolveLibraryReference(mAccessorLibrary, input.mAccessor); // resolve accessor's data pointer as well, if necessary const Accessor* acc = input.mResolved; - if( !acc->mData) - acc->mData = &ResolveLibraryReference( mDataLibrary, acc->mSource); + if (!acc->mData) + acc->mData = &ResolveLibraryReference(mDataLibrary, acc->mSource); } // and the same for the per-index channels - for( std::vector::iterator it = pPerIndexChannels.begin(); it != pPerIndexChannels.end(); ++it) + for (std::vector::iterator it = pPerIndexChannels.begin(); it != pPerIndexChannels.end(); ++it) { InputChannel& input = *it; - if( input.mResolved) + if (input.mResolved) continue; // ignore vertex pointer, it doesn't refer to an accessor - if( input.mType == IT_Vertex) + if (input.mType == IT_Vertex) { // warn if the vertex channel does not refer to the element in the same mesh - if( input.mAccessor != pMesh->mVertexID) - ThrowException( "Unsupported vertex referencing scheme."); + if (input.mAccessor != pMesh->mVertexID) + ThrowException("Unsupported vertex referencing scheme."); continue; } // find accessor - input.mResolved = &ResolveLibraryReference( mAccessorLibrary, input.mAccessor); + input.mResolved = &ResolveLibraryReference(mAccessorLibrary, input.mAccessor); // resolve accessor's data pointer as well, if necessary const Accessor* acc = input.mResolved; - if( !acc->mData) - acc->mData = &ResolveLibraryReference( mDataLibrary, acc->mSource); + if (!acc->mData) + acc->mData = &ResolveLibraryReference(mDataLibrary, acc->mSource); } // For continued primitives, the given count does not come all in one

, but only one primitive per

size_t numPrimitives = pNumPrimitives; - if( pPrimType == Prim_TriFans || pPrimType == Prim_Polygon) + if (pPrimType == Prim_TriFans || pPrimType == Prim_Polygon) numPrimitives = 1; // For continued primitives, the given count is actually the number of

's inside the parent tag - if ( pPrimType == Prim_TriStrips){ + if (pPrimType == Prim_TriStrips) { size_t numberOfVertices = indices.size() / numOffsets; numPrimitives = numberOfVertices - 2; } @@ -2494,66 +2592,66 @@ size_t ColladaParser::ReadPrimitives( Mesh* pMesh, std::vector& pP numPrimitives = numberOfVertices - 1; } - pMesh->mFaceSize.reserve( numPrimitives); - pMesh->mFacePosIndices.reserve( indices.size() / numOffsets); + pMesh->mFaceSize.reserve(numPrimitives); + pMesh->mFacePosIndices.reserve(indices.size() / numOffsets); size_t polylistStartVertex = 0; for (size_t currentPrimitive = 0; currentPrimitive < numPrimitives; currentPrimitive++) { // determine number of points for this primitive size_t numPoints = 0; - switch( pPrimType) + switch (pPrimType) { - case Prim_Lines: - numPoints = 2; - for (size_t currentVertex = 0; currentVertex < numPoints; currentVertex++) - CopyVertex(currentVertex, numOffsets, numPoints, perVertexOffset, pMesh, pPerIndexChannels, currentPrimitive, indices); - break; - case Prim_LineStrip: - numPoints = 2; - for (size_t currentVertex = 0; currentVertex < numPoints; currentVertex++) - CopyVertex(currentVertex, numOffsets, 1, perVertexOffset, pMesh, pPerIndexChannels, currentPrimitive, indices); - break; - case Prim_Triangles: - numPoints = 3; - for (size_t currentVertex = 0; currentVertex < numPoints; currentVertex++) - CopyVertex(currentVertex, numOffsets, numPoints, perVertexOffset, pMesh, pPerIndexChannels, currentPrimitive, indices); - break; - case Prim_TriStrips: - numPoints = 3; - ReadPrimTriStrips(numOffsets, perVertexOffset, pMesh, pPerIndexChannels, currentPrimitive, indices); - break; - case Prim_Polylist: - numPoints = pVCount[currentPrimitive]; - for (size_t currentVertex = 0; currentVertex < numPoints; currentVertex++) - CopyVertex(polylistStartVertex + currentVertex, numOffsets, 1, perVertexOffset, pMesh, pPerIndexChannels, 0, indices); - polylistStartVertex += numPoints; - break; - case Prim_TriFans: - case Prim_Polygon: - numPoints = indices.size() / numOffsets; - for (size_t currentVertex = 0; currentVertex < numPoints; currentVertex++) - CopyVertex(currentVertex, numOffsets, numPoints, perVertexOffset, pMesh, pPerIndexChannels, currentPrimitive, indices); - break; - default: - // LineStrip is not supported due to expected index unmangling - ThrowException( "Unsupported primitive type."); - break; + case Prim_Lines: + numPoints = 2; + for (size_t currentVertex = 0; currentVertex < numPoints; currentVertex++) + CopyVertex(currentVertex, numOffsets, numPoints, perVertexOffset, pMesh, pPerIndexChannels, currentPrimitive, indices); + break; + case Prim_LineStrip: + numPoints = 2; + for (size_t currentVertex = 0; currentVertex < numPoints; currentVertex++) + CopyVertex(currentVertex, numOffsets, 1, perVertexOffset, pMesh, pPerIndexChannels, currentPrimitive, indices); + break; + case Prim_Triangles: + numPoints = 3; + for (size_t currentVertex = 0; currentVertex < numPoints; currentVertex++) + CopyVertex(currentVertex, numOffsets, numPoints, perVertexOffset, pMesh, pPerIndexChannels, currentPrimitive, indices); + break; + case Prim_TriStrips: + numPoints = 3; + ReadPrimTriStrips(numOffsets, perVertexOffset, pMesh, pPerIndexChannels, currentPrimitive, indices); + break; + case Prim_Polylist: + numPoints = pVCount[currentPrimitive]; + for (size_t currentVertex = 0; currentVertex < numPoints; currentVertex++) + CopyVertex(polylistStartVertex + currentVertex, numOffsets, 1, perVertexOffset, pMesh, pPerIndexChannels, 0, indices); + polylistStartVertex += numPoints; + break; + case Prim_TriFans: + case Prim_Polygon: + numPoints = indices.size() / numOffsets; + for (size_t currentVertex = 0; currentVertex < numPoints; currentVertex++) + CopyVertex(currentVertex, numOffsets, numPoints, perVertexOffset, pMesh, pPerIndexChannels, currentPrimitive, indices); + break; + default: + // LineStrip is not supported due to expected index unmangling + ThrowException("Unsupported primitive type."); + break; } // store the face size to later reconstruct the face from - pMesh->mFaceSize.push_back( numPoints); + pMesh->mFaceSize.push_back(numPoints); } // if I ever get my hands on that guy who invented this steaming pile of indirection... - TestClosing( "p"); + TestClosing("p"); return numPrimitives; } ///@note This function willn't work correctly if both PerIndex and PerVertex channels have same channels. ///For example if TEXCOORD present in both and tags this function will create wrong uv coordinates. ///It's not clear from COLLADA documentation is this allowed or not. For now only exporter fixed to avoid such behavior -void ColladaParser::CopyVertex(size_t currentVertex, size_t numOffsets, size_t numPoints, size_t perVertexOffset, Mesh* pMesh, std::vector& pPerIndexChannels, size_t currentPrimitive, const std::vector& indices){ +void ColladaParser::CopyVertex(size_t currentVertex, size_t numOffsets, size_t numPoints, size_t perVertexOffset, Mesh* pMesh, std::vector& pPerIndexChannels, size_t currentPrimitive, const std::vector& indices) { // calculate the base offset of the vertex whose attributes we ant to copy size_t baseOffset = currentPrimitive * numOffsets * numPoints + currentVertex * numOffsets; @@ -2571,8 +2669,8 @@ void ColladaParser::CopyVertex(size_t currentVertex, size_t numOffsets, size_t n pMesh->mFacePosIndices.push_back(indices[baseOffset + perVertexOffset]); } -void ColladaParser::ReadPrimTriStrips(size_t numOffsets, size_t perVertexOffset, Mesh* pMesh, std::vector& pPerIndexChannels, size_t currentPrimitive, const std::vector& indices){ - if (currentPrimitive % 2 != 0){ +void ColladaParser::ReadPrimTriStrips(size_t numOffsets, size_t perVertexOffset, Mesh* pMesh, std::vector& pPerIndexChannels, size_t currentPrimitive, const std::vector& indices) { + if (currentPrimitive % 2 != 0) { //odd tristrip triangles need their indices mangled, to preserve winding direction CopyVertex(1, numOffsets, 1, perVertexOffset, pMesh, pPerIndexChannels, currentPrimitive, indices); CopyVertex(0, numOffsets, 1, perVertexOffset, pMesh, pPerIndexChannels, currentPrimitive, indices); @@ -2587,108 +2685,110 @@ void ColladaParser::ReadPrimTriStrips(size_t numOffsets, size_t perVertexOffset, // ------------------------------------------------------------------------------------------------ // Extracts a single object from an input channel and stores it in the appropriate mesh data array -void ColladaParser::ExtractDataObjectFromChannel( const InputChannel& pInput, size_t pLocalIndex, Mesh* pMesh) +void ColladaParser::ExtractDataObjectFromChannel(const InputChannel& pInput, size_t pLocalIndex, Mesh* pMesh) { // ignore vertex referrer - we handle them that separate - if( pInput.mType == IT_Vertex) + if (pInput.mType == IT_Vertex) return; const Accessor& acc = *pInput.mResolved; - if( pLocalIndex >= acc.mCount) - ThrowException( format() << "Invalid data index (" << pLocalIndex << "/" << acc.mCount << ") in primitive specification" ); + if (pLocalIndex >= acc.mCount) + ThrowException(format() << "Invalid data index (" << pLocalIndex << "/" << acc.mCount << ") in primitive specification"); // get a pointer to the start of the data object referred to by the accessor and the local index - const ai_real* dataObject = &(acc.mData->mValues[0]) + acc.mOffset + pLocalIndex* acc.mStride; + const ai_real* dataObject = &(acc.mData->mValues[0]) + acc.mOffset + pLocalIndex * acc.mStride; // assemble according to the accessors component sub-offset list. We don't care, yet, // what kind of object exactly we're extracting here ai_real obj[4]; - for( size_t c = 0; c < 4; ++c) + for (size_t c = 0; c < 4; ++c) obj[c] = dataObject[acc.mSubOffset[c]]; // now we reinterpret it according to the type we're reading here - switch( pInput.mType) + switch (pInput.mType) { - case IT_Position: // ignore all position streams except 0 - there can be only one position - if( pInput.mIndex == 0) - pMesh->mPositions.push_back( aiVector3D( obj[0], obj[1], obj[2])); - else - ASSIMP_LOG_ERROR("Collada: just one vertex position stream supported"); - break; - case IT_Normal: - // pad to current vertex count if necessary - if( pMesh->mNormals.size() < pMesh->mPositions.size()-1) - pMesh->mNormals.insert( pMesh->mNormals.end(), pMesh->mPositions.size() - pMesh->mNormals.size() - 1, aiVector3D( 0, 1, 0)); + case IT_Position: // ignore all position streams except 0 - there can be only one position + if (pInput.mIndex == 0) + pMesh->mPositions.push_back(aiVector3D(obj[0], obj[1], obj[2])); + else + ASSIMP_LOG_ERROR("Collada: just one vertex position stream supported"); + break; + case IT_Normal: + // pad to current vertex count if necessary + if (pMesh->mNormals.size() < pMesh->mPositions.size() - 1) + pMesh->mNormals.insert(pMesh->mNormals.end(), pMesh->mPositions.size() - pMesh->mNormals.size() - 1, aiVector3D(0, 1, 0)); - // ignore all normal streams except 0 - there can be only one normal - if( pInput.mIndex == 0) - pMesh->mNormals.push_back( aiVector3D( obj[0], obj[1], obj[2])); - else - ASSIMP_LOG_ERROR("Collada: just one vertex normal stream supported"); - break; - case IT_Tangent: - // pad to current vertex count if necessary - if( pMesh->mTangents.size() < pMesh->mPositions.size()-1) - pMesh->mTangents.insert( pMesh->mTangents.end(), pMesh->mPositions.size() - pMesh->mTangents.size() - 1, aiVector3D( 1, 0, 0)); + // ignore all normal streams except 0 - there can be only one normal + if (pInput.mIndex == 0) + pMesh->mNormals.push_back(aiVector3D(obj[0], obj[1], obj[2])); + else + ASSIMP_LOG_ERROR("Collada: just one vertex normal stream supported"); + break; + case IT_Tangent: + // pad to current vertex count if necessary + if (pMesh->mTangents.size() < pMesh->mPositions.size() - 1) + pMesh->mTangents.insert(pMesh->mTangents.end(), pMesh->mPositions.size() - pMesh->mTangents.size() - 1, aiVector3D(1, 0, 0)); - // ignore all tangent streams except 0 - there can be only one tangent - if( pInput.mIndex == 0) - pMesh->mTangents.push_back( aiVector3D( obj[0], obj[1], obj[2])); - else - ASSIMP_LOG_ERROR("Collada: just one vertex tangent stream supported"); - break; - case IT_Bitangent: - // pad to current vertex count if necessary - if( pMesh->mBitangents.size() < pMesh->mPositions.size()-1) - pMesh->mBitangents.insert( pMesh->mBitangents.end(), pMesh->mPositions.size() - pMesh->mBitangents.size() - 1, aiVector3D( 0, 0, 1)); + // ignore all tangent streams except 0 - there can be only one tangent + if (pInput.mIndex == 0) + pMesh->mTangents.push_back(aiVector3D(obj[0], obj[1], obj[2])); + else + ASSIMP_LOG_ERROR("Collada: just one vertex tangent stream supported"); + break; + case IT_Bitangent: + // pad to current vertex count if necessary + if (pMesh->mBitangents.size() < pMesh->mPositions.size() - 1) + pMesh->mBitangents.insert(pMesh->mBitangents.end(), pMesh->mPositions.size() - pMesh->mBitangents.size() - 1, aiVector3D(0, 0, 1)); - // ignore all bitangent streams except 0 - there can be only one bitangent - if( pInput.mIndex == 0) - pMesh->mBitangents.push_back( aiVector3D( obj[0], obj[1], obj[2])); - else - ASSIMP_LOG_ERROR("Collada: just one vertex bitangent stream supported"); - break; - case IT_Texcoord: - // up to 4 texture coord sets are fine, ignore the others - if( pInput.mIndex < AI_MAX_NUMBER_OF_TEXTURECOORDS) + // ignore all bitangent streams except 0 - there can be only one bitangent + if (pInput.mIndex == 0) + pMesh->mBitangents.push_back(aiVector3D(obj[0], obj[1], obj[2])); + else + ASSIMP_LOG_ERROR("Collada: just one vertex bitangent stream supported"); + break; + case IT_Texcoord: + // up to 4 texture coord sets are fine, ignore the others + if (pInput.mIndex < AI_MAX_NUMBER_OF_TEXTURECOORDS) + { + // pad to current vertex count if necessary + if (pMesh->mTexCoords[pInput.mIndex].size() < pMesh->mPositions.size() - 1) + pMesh->mTexCoords[pInput.mIndex].insert(pMesh->mTexCoords[pInput.mIndex].end(), + pMesh->mPositions.size() - pMesh->mTexCoords[pInput.mIndex].size() - 1, aiVector3D(0, 0, 0)); + + pMesh->mTexCoords[pInput.mIndex].push_back(aiVector3D(obj[0], obj[1], obj[2])); + if (0 != acc.mSubOffset[2] || 0 != acc.mSubOffset[3]) /* hack ... consider cleaner solution */ + pMesh->mNumUVComponents[pInput.mIndex] = 3; + } + else + { + ASSIMP_LOG_ERROR("Collada: too many texture coordinate sets. Skipping."); + } + break; + case IT_Color: + // up to 4 color sets are fine, ignore the others + if (pInput.mIndex < AI_MAX_NUMBER_OF_COLOR_SETS) + { + // pad to current vertex count if necessary + if (pMesh->mColors[pInput.mIndex].size() < pMesh->mPositions.size() - 1) + pMesh->mColors[pInput.mIndex].insert(pMesh->mColors[pInput.mIndex].end(), + pMesh->mPositions.size() - pMesh->mColors[pInput.mIndex].size() - 1, aiColor4D(0, 0, 0, 1)); + + aiColor4D result(0, 0, 0, 1); + for (size_t i = 0; i < pInput.mResolved->mSize; ++i) { - // pad to current vertex count if necessary - if( pMesh->mTexCoords[pInput.mIndex].size() < pMesh->mPositions.size()-1) - pMesh->mTexCoords[pInput.mIndex].insert( pMesh->mTexCoords[pInput.mIndex].end(), - pMesh->mPositions.size() - pMesh->mTexCoords[pInput.mIndex].size() - 1, aiVector3D( 0, 0, 0)); - - pMesh->mTexCoords[pInput.mIndex].push_back( aiVector3D( obj[0], obj[1], obj[2])); - if (0 != acc.mSubOffset[2] || 0 != acc.mSubOffset[3]) /* hack ... consider cleaner solution */ - pMesh->mNumUVComponents[pInput.mIndex]=3; - } else - { - ASSIMP_LOG_ERROR("Collada: too many texture coordinate sets. Skipping."); + result[static_cast(i)] = obj[pInput.mResolved->mSubOffset[i]]; } - break; - case IT_Color: - // up to 4 color sets are fine, ignore the others - if( pInput.mIndex < AI_MAX_NUMBER_OF_COLOR_SETS) - { - // pad to current vertex count if necessary - if( pMesh->mColors[pInput.mIndex].size() < pMesh->mPositions.size()-1) - pMesh->mColors[pInput.mIndex].insert( pMesh->mColors[pInput.mIndex].end(), - pMesh->mPositions.size() - pMesh->mColors[pInput.mIndex].size() - 1, aiColor4D( 0, 0, 0, 1)); + pMesh->mColors[pInput.mIndex].push_back(result); + } + else + { + ASSIMP_LOG_ERROR("Collada: too many vertex color sets. Skipping."); + } - aiColor4D result(0, 0, 0, 1); - for (size_t i = 0; i < pInput.mResolved->mSize; ++i) - { - result[static_cast(i)] = obj[pInput.mResolved->mSubOffset[i]]; - } - pMesh->mColors[pInput.mIndex].push_back(result); - } else - { - ASSIMP_LOG_ERROR("Collada: too many vertex color sets. Skipping."); - } - - break; - default: - // IT_Invalid and IT_Vertex - ai_assert(false && "shouldn't ever get here"); + break; + default: + // IT_Invalid and IT_Vertex + ai_assert(false && "shouldn't ever get here"); } } @@ -2696,25 +2796,25 @@ void ColladaParser::ExtractDataObjectFromChannel( const InputChannel& pInput, si // Reads the library of node hierarchies and scene parts void ColladaParser::ReadSceneLibrary() { - if( mReader->isEmptyElement()) + if (mReader->isEmptyElement()) return; - while( mReader->read()) + while (mReader->read()) { - if( mReader->getNodeType() == irr::io::EXN_ELEMENT) + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) { // a visual scene - generate root node under its ID and let ReadNode() do the recursive work - if( IsElement( "visual_scene")) + if (IsElement("visual_scene")) { // read ID. Is optional according to the spec, but how on earth should a scene_instance refer to it then? - int indexID = GetAttribute( "id"); - const char* attrID = mReader->getAttributeValue( indexID); + int indexID = GetAttribute("id"); + const char* attrID = mReader->getAttributeValue(indexID); // read name if given. - int indexName = TestAttribute( "name"); + int indexName = TestAttribute("name"); const char* attrName = "unnamed"; - if( indexName > -1) - attrName = mReader->getAttributeValue( indexName); + if (indexName > -1) + attrName = mReader->getAttributeValue(indexName); // create a node and store it in the library under its ID Node* node = new Node; @@ -2722,55 +2822,56 @@ void ColladaParser::ReadSceneLibrary() node->mName = attrName; mNodeLibrary[node->mID] = node; - ReadSceneNode( node); - } else + ReadSceneNode(node); + } + else { // ignore the rest SkipElement(); } } - else if( mReader->getNodeType() == irr::io::EXN_ELEMENT_END) + else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { - if( strcmp( mReader->getNodeName(), "library_visual_scenes") == 0) + if (strcmp(mReader->getNodeName(), "library_visual_scenes") == 0) //ThrowException( "Expected end of \"library_visual_scenes\" element."); - break; + break; } } } // ------------------------------------------------------------------------------------------------ // Reads a scene node's contents including children and stores it in the given node -void ColladaParser::ReadSceneNode( Node* pNode) +void ColladaParser::ReadSceneNode(Node* pNode) { // quit immediately on elements - if( mReader->isEmptyElement()) + if (mReader->isEmptyElement()) return; - while( mReader->read()) + while (mReader->read()) { - if( mReader->getNodeType() == irr::io::EXN_ELEMENT) + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) { - if( IsElement( "node")) + if (IsElement("node")) { Node* child = new Node; - int attrID = TestAttribute( "id"); - if( attrID > -1) - child->mID = mReader->getAttributeValue( attrID); - int attrSID = TestAttribute( "sid"); - if( attrSID > -1) - child->mSID = mReader->getAttributeValue( attrSID); + int attrID = TestAttribute("id"); + if (attrID > -1) + child->mID = mReader->getAttributeValue(attrID); + int attrSID = TestAttribute("sid"); + if (attrSID > -1) + child->mSID = mReader->getAttributeValue(attrSID); - int attrName = TestAttribute( "name"); - if( attrName > -1) - child->mName = mReader->getAttributeValue( attrName); + int attrName = TestAttribute("name"); + if (attrName > -1) + child->mName = mReader->getAttributeValue(attrName); // TODO: (thom) support SIDs // ai_assert( TestAttribute( "sid") == -1); if (pNode) { - pNode->mChildren.push_back( child); + pNode->mChildren.push_back(child); child->mParent = pNode; } else @@ -2781,26 +2882,26 @@ void ColladaParser::ReadSceneNode( Node* pNode) } // read on recursively from there - ReadSceneNode( child); + ReadSceneNode(child); continue; } // For any further stuff we need a valid node to work on else if (!pNode) continue; - if( IsElement( "lookat")) - ReadNodeTransformation( pNode, TF_LOOKAT); - else if( IsElement( "matrix")) - ReadNodeTransformation( pNode, TF_MATRIX); - else if( IsElement( "rotate")) - ReadNodeTransformation( pNode, TF_ROTATE); - else if( IsElement( "scale")) - ReadNodeTransformation( pNode, TF_SCALE); - else if( IsElement( "skew")) - ReadNodeTransformation( pNode, TF_SKEW); - else if( IsElement( "translate")) - ReadNodeTransformation( pNode, TF_TRANSLATE); - else if( IsElement( "render") && pNode->mParent == NULL && 0 == pNode->mPrimaryCamera.length()) + if (IsElement("lookat")) + ReadNodeTransformation(pNode, TF_LOOKAT); + else if (IsElement("matrix")) + ReadNodeTransformation(pNode, TF_MATRIX); + else if (IsElement("rotate")) + ReadNodeTransformation(pNode, TF_ROTATE); + else if (IsElement("scale")) + ReadNodeTransformation(pNode, TF_SCALE); + else if (IsElement("skew")) + ReadNodeTransformation(pNode, TF_SKEW); + else if (IsElement("translate")) + ReadNodeTransformation(pNode, TF_TRANSLATE); + else if (IsElement("render") && pNode->mParent == NULL && 0 == pNode->mPrimaryCamera.length()) { // ... scene evaluation or, in other words, postprocessing pipeline, // or, again in other words, a turing-complete description how to @@ -2813,14 +2914,14 @@ void ColladaParser::ReadSceneNode( Node* pNode) if (s[0] != '#') ASSIMP_LOG_ERROR("Collada: Unresolved reference format of camera"); else - pNode->mPrimaryCamera = s+1; + pNode->mPrimaryCamera = s + 1; } } - else if( IsElement( "instance_node")) + else if (IsElement("instance_node")) { // find the node in the library - int attrID = TestAttribute( "url"); - if( attrID != -1) + int attrID = TestAttribute("url"); + if (attrID != -1) { const char* s = mReader->getAttributeValue(attrID); if (s[0] != '#') @@ -2828,16 +2929,16 @@ void ColladaParser::ReadSceneNode( Node* pNode) else { pNode->mNodeInstances.push_back(NodeInstance()); - pNode->mNodeInstances.back().mNode = s+1; + pNode->mNodeInstances.back().mNode = s + 1; } } } - else if( IsElement( "instance_geometry") || IsElement( "instance_controller")) + else if (IsElement("instance_geometry") || IsElement("instance_controller")) { // Reference to a mesh or controller, with possible material associations - ReadNodeGeometry( pNode); + ReadNodeGeometry(pNode); } - else if( IsElement( "instance_light")) + else if (IsElement("instance_light")) { // Reference to a light, name given in 'url' attribute int attrID = TestAttribute("url"); @@ -2845,15 +2946,15 @@ void ColladaParser::ReadSceneNode( Node* pNode) ASSIMP_LOG_WARN("Collada: Expected url attribute in element"); else { - const char* url = mReader->getAttributeValue( attrID); - if( url[0] != '#') - ThrowException( "Unknown reference format in element"); + const char* url = mReader->getAttributeValue(attrID); + if (url[0] != '#') + ThrowException("Unknown reference format in element"); pNode->mLights.push_back(LightInstance()); - pNode->mLights.back().mLight = url+1; + pNode->mLights.back().mLight = url + 1; } } - else if( IsElement( "instance_camera")) + else if (IsElement("instance_camera")) { // Reference to a camera, name given in 'url' attribute int attrID = TestAttribute("url"); @@ -2861,12 +2962,12 @@ void ColladaParser::ReadSceneNode( Node* pNode) ASSIMP_LOG_WARN("Collada: Expected url attribute in element"); else { - const char* url = mReader->getAttributeValue( attrID); - if( url[0] != '#') - ThrowException( "Unknown reference format in element"); + const char* url = mReader->getAttributeValue(attrID); + if (url[0] != '#') + ThrowException("Unknown reference format in element"); pNode->mCameras.push_back(CameraInstance()); - pNode->mCameras.back().mCamera = url+1; + pNode->mCameras.back().mCamera = url + 1; } } else @@ -2875,7 +2976,7 @@ void ColladaParser::ReadSceneNode( Node* pNode) SkipElement(); } } - else if( mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { + else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { break; } } @@ -2883,9 +2984,9 @@ void ColladaParser::ReadSceneNode( Node* pNode) // ------------------------------------------------------------------------------------------------ // Reads a node transformation entry of the given type and adds it to the given node's transformation list. -void ColladaParser::ReadNodeTransformation( Node* pNode, TransformType pType) +void ColladaParser::ReadNodeTransformation(Node* pNode, TransformType pType) { - if( mReader->isEmptyElement()) + if (mReader->isEmptyElement()) return; std::string tagName = mReader->getNodeName(); @@ -2894,38 +2995,38 @@ void ColladaParser::ReadNodeTransformation( Node* pNode, TransformType pType) tf.mType = pType; // read SID - int indexSID = TestAttribute( "sid"); - if( indexSID >= 0) - tf.mID = mReader->getAttributeValue( indexSID); + int indexSID = TestAttribute("sid"); + if (indexSID >= 0) + tf.mID = mReader->getAttributeValue(indexSID); // how many parameters to read per transformation type static const unsigned int sNumParameters[] = { 9, 4, 3, 3, 7, 16 }; const char* content = GetTextContent(); // read as many parameters and store in the transformation - for( unsigned int a = 0; a < sNumParameters[pType]; a++) + for (unsigned int a = 0; a < sNumParameters[pType]; a++) { // read a number - content = fast_atoreal_move( content, tf.f[a]); + content = fast_atoreal_move(content, tf.f[a]); // skip whitespace after it - SkipSpacesAndLineEnd( &content); + SkipSpacesAndLineEnd(&content); } // place the transformation at the queue of the node - pNode->mTransforms.push_back( tf); + pNode->mTransforms.push_back(tf); // and consume the closing tag - TestClosing( tagName.c_str()); + TestClosing(tagName.c_str()); } // ------------------------------------------------------------------------------------------------ // Processes bind_vertex_input and bind elements -void ColladaParser::ReadMaterialVertexInputBinding( Collada::SemanticMappingTable& tbl) +void ColladaParser::ReadMaterialVertexInputBinding(Collada::SemanticMappingTable& tbl) { - while( mReader->read()) + while (mReader->read()) { - if( mReader->getNodeType() == irr::io::EXN_ELEMENT) { - if( IsElement( "bind_vertex_input")) + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) { + if (IsElement("bind_vertex_input")) { Collada::InputSemanticMapEntry vn; @@ -2935,7 +3036,7 @@ void ColladaParser::ReadMaterialVertexInputBinding( Collada::SemanticMappingTabl // input semantic n = GetAttribute("input_semantic"); - vn.mType = GetTypeForSemantic( mReader->getAttributeValue(n) ); + vn.mType = GetTypeForSemantic(mReader->getAttributeValue(n)); // index of input set n = TestAttribute("input_set"); @@ -2944,12 +3045,12 @@ void ColladaParser::ReadMaterialVertexInputBinding( Collada::SemanticMappingTabl tbl.mMap[s] = vn; } - else if( IsElement( "bind")) { + else if (IsElement("bind")) { ASSIMP_LOG_WARN("Collada: Found unsupported element"); } } - else if( mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { - if( strcmp( mReader->getNodeName(), "instance_material") == 0) + else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { + if (strcmp(mReader->getNodeName(), "instance_material") == 0) break; } } @@ -2957,90 +3058,91 @@ void ColladaParser::ReadMaterialVertexInputBinding( Collada::SemanticMappingTabl // ------------------------------------------------------------------------------------------------ // Reads a mesh reference in a node and adds it to the node's mesh list -void ColladaParser::ReadNodeGeometry( Node* pNode) +void ColladaParser::ReadNodeGeometry(Node* pNode) { // referred mesh is given as an attribute of the element - int attrUrl = GetAttribute( "url"); - const char* url = mReader->getAttributeValue( attrUrl); - if( url[0] != '#') - ThrowException( "Unknown reference format"); + int attrUrl = GetAttribute("url"); + const char* url = mReader->getAttributeValue(attrUrl); + if (url[0] != '#') + ThrowException("Unknown reference format"); Collada::MeshInstance instance; - instance.mMeshOrController = url+1; // skipping the leading # + instance.mMeshOrController = url + 1; // skipping the leading # - if( !mReader->isEmptyElement()) + if (!mReader->isEmptyElement()) { // read material associations. Ignore additional elements in between - while( mReader->read()) + while (mReader->read()) { - if( mReader->getNodeType() == irr::io::EXN_ELEMENT) + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) { - if( IsElement( "instance_material")) + if (IsElement("instance_material")) { // read ID of the geometry subgroup and the target material - int attrGroup = GetAttribute( "symbol"); - std::string group = mReader->getAttributeValue( attrGroup); - int attrMaterial = GetAttribute( "target"); - const char* urlMat = mReader->getAttributeValue( attrMaterial); + int attrGroup = GetAttribute("symbol"); + std::string group = mReader->getAttributeValue(attrGroup); + int attrMaterial = GetAttribute("target"); + const char* urlMat = mReader->getAttributeValue(attrMaterial); Collada::SemanticMappingTable s; - if( urlMat[0] == '#') + if (urlMat[0] == '#') urlMat++; s.mMatName = urlMat; // resolve further material details + THIS UGLY AND NASTY semantic mapping stuff - if( !mReader->isEmptyElement()) + if (!mReader->isEmptyElement()) ReadMaterialVertexInputBinding(s); // store the association instance.mMaterials[group] = s; } } - else if( mReader->getNodeType() == irr::io::EXN_ELEMENT_END) + else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { - if( strcmp( mReader->getNodeName(), "instance_geometry") == 0 - || strcmp( mReader->getNodeName(), "instance_controller") == 0) + if (strcmp(mReader->getNodeName(), "instance_geometry") == 0 + || strcmp(mReader->getNodeName(), "instance_controller") == 0) break; } } } // store it - pNode->mMeshes.push_back( instance); + pNode->mMeshes.push_back(instance); } // ------------------------------------------------------------------------------------------------ // Reads the collada scene void ColladaParser::ReadScene() { - if( mReader->isEmptyElement()) + if (mReader->isEmptyElement()) return; - while( mReader->read()) + while (mReader->read()) { - if( mReader->getNodeType() == irr::io::EXN_ELEMENT) { - if( IsElement( "instance_visual_scene")) + if (mReader->getNodeType() == irr::io::EXN_ELEMENT) { + if (IsElement("instance_visual_scene")) { // should be the first and only occurrence - if( mRootNode) - ThrowException( "Invalid scene containing multiple root nodes in element"); + if (mRootNode) + ThrowException("Invalid scene containing multiple root nodes in element"); // read the url of the scene to instance. Should be of format "#some_name" - int urlIndex = GetAttribute( "url"); - const char* url = mReader->getAttributeValue( urlIndex); - if( url[0] != '#') - ThrowException( "Unknown reference format in element"); + int urlIndex = GetAttribute("url"); + const char* url = mReader->getAttributeValue(urlIndex); + if (url[0] != '#') + ThrowException("Unknown reference format in element"); // find the referred scene, skip the leading # - NodeLibrary::const_iterator sit = mNodeLibrary.find( url+1); - if( sit == mNodeLibrary.end()) - ThrowException( "Unable to resolve visual_scene reference \"" + std::string(url) + "\" in element."); + NodeLibrary::const_iterator sit = mNodeLibrary.find(url + 1); + if (sit == mNodeLibrary.end()) + ThrowException("Unable to resolve visual_scene reference \"" + std::string(url) + "\" in element."); mRootNode = sit->second; - } else { + } + else { SkipElement(); } } - else if( mReader->getNodeType() == irr::io::EXN_ELEMENT_END){ + else if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { break; } } @@ -3048,23 +3150,23 @@ void ColladaParser::ReadScene() // ------------------------------------------------------------------------------------------------ // Aborts the file reading with an exception -AI_WONT_RETURN void ColladaParser::ThrowException( const std::string& pError) const +AI_WONT_RETURN void ColladaParser::ThrowException(const std::string& pError) const { - throw DeadlyImportError( format() << "Collada: " << mFileName << " - " << pError ); + throw DeadlyImportError(format() << "Collada: " << mFileName << " - " << pError); } -void ColladaParser::ReportWarning(const char* msg,...) +void ColladaParser::ReportWarning(const char* msg, ...) { ai_assert(NULL != msg); va_list args; - va_start(args,msg); + va_start(args, msg); char szBuffer[3000]; - const int iLen = vsprintf(szBuffer,msg,args); + const int iLen = vsprintf(szBuffer, msg, args); ai_assert(iLen > 0); va_end(args); - ASSIMP_LOG_WARN_F("Validation warning: ", std::string(szBuffer,iLen)); + ASSIMP_LOG_WARN_F("Validation warning: ", std::string(szBuffer, iLen)); } // ------------------------------------------------------------------------------------------------ @@ -3072,84 +3174,84 @@ void ColladaParser::ReportWarning(const char* msg,...) void ColladaParser::SkipElement() { // nothing to skip if it's an - if( mReader->isEmptyElement()) + if (mReader->isEmptyElement()) return; // reroute - SkipElement( mReader->getNodeName()); + SkipElement(mReader->getNodeName()); } // ------------------------------------------------------------------------------------------------ // Skips all data until the end node of the given element -void ColladaParser::SkipElement( const char* pElement) +void ColladaParser::SkipElement(const char* pElement) { // copy the current node's name because it'a pointer to the reader's internal buffer, // which is going to change with the upcoming parsing std::string element = pElement; - while( mReader->read()) + while (mReader->read()) { - if( mReader->getNodeType() == irr::io::EXN_ELEMENT_END) - if( mReader->getNodeName() == element) + if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) + if (mReader->getNodeName() == element) break; } } // ------------------------------------------------------------------------------------------------ // Tests for an opening element of the given name, throws an exception if not found -void ColladaParser::TestOpening( const char* pName) +void ColladaParser::TestOpening(const char* pName) { // read element start - if( !mReader->read()) - ThrowException( format() << "Unexpected end of file while beginning of <" << pName << "> element." ); + if (!mReader->read()) + ThrowException(format() << "Unexpected end of file while beginning of <" << pName << "> element."); // whitespace in front is ok, just read again if found - if( mReader->getNodeType() == irr::io::EXN_TEXT) - if( !mReader->read()) - ThrowException( format() << "Unexpected end of file while reading beginning of <" << pName << "> element." ); + if (mReader->getNodeType() == irr::io::EXN_TEXT) + if (!mReader->read()) + ThrowException(format() << "Unexpected end of file while reading beginning of <" << pName << "> element."); - if( mReader->getNodeType() != irr::io::EXN_ELEMENT || strcmp( mReader->getNodeName(), pName) != 0) - ThrowException( format() << "Expected start of <" << pName << "> element." ); + if (mReader->getNodeType() != irr::io::EXN_ELEMENT || strcmp(mReader->getNodeName(), pName) != 0) + ThrowException(format() << "Expected start of <" << pName << "> element."); } // ------------------------------------------------------------------------------------------------ // Tests for the closing tag of the given element, throws an exception if not found -void ColladaParser::TestClosing( const char* pName) +void ColladaParser::TestClosing(const char* pName) { // check if we're already on the closing tag and return right away - if( mReader->getNodeType() == irr::io::EXN_ELEMENT_END && strcmp( mReader->getNodeName(), pName) == 0) + if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END && strcmp(mReader->getNodeName(), pName) == 0) return; // if not, read some more - if( !mReader->read()) - ThrowException( format() << "Unexpected end of file while reading end of <" << pName << "> element." ); + if (!mReader->read()) + ThrowException(format() << "Unexpected end of file while reading end of <" << pName << "> element."); // whitespace in front is ok, just read again if found - if( mReader->getNodeType() == irr::io::EXN_TEXT) - if( !mReader->read()) - ThrowException( format() << "Unexpected end of file while reading end of <" << pName << "> element." ); + if (mReader->getNodeType() == irr::io::EXN_TEXT) + if (!mReader->read()) + ThrowException(format() << "Unexpected end of file while reading end of <" << pName << "> element."); // but this has the be the closing tag, or we're lost - if( mReader->getNodeType() != irr::io::EXN_ELEMENT_END || strcmp( mReader->getNodeName(), pName) != 0) - ThrowException( format() << "Expected end of <" << pName << "> element." ); + if (mReader->getNodeType() != irr::io::EXN_ELEMENT_END || strcmp(mReader->getNodeName(), pName) != 0) + ThrowException(format() << "Expected end of <" << pName << "> element."); } // ------------------------------------------------------------------------------------------------ // Returns the index of the named attribute or -1 if not found. Does not throw, therefore useful for optional attributes -int ColladaParser::GetAttribute( const char* pAttr) const +int ColladaParser::GetAttribute(const char* pAttr) const { - int index = TestAttribute( pAttr); - if( index != -1) + int index = TestAttribute(pAttr); + if (index != -1) return index; // attribute not found -> throw an exception - ThrowException( format() << "Expected attribute \"" << pAttr << "\" for element <" << mReader->getNodeName() << ">." ); + ThrowException(format() << "Expected attribute \"" << pAttr << "\" for element <" << mReader->getNodeName() << ">."); return -1; } // ------------------------------------------------------------------------------------------------ // Tests the present element for the presence of one attribute, returns its index or throws an exception if not found -int ColladaParser::TestAttribute( const char* pAttr) const +int ColladaParser::TestAttribute(const char* pAttr) const { - for( int a = 0; a < mReader->getAttributeCount(); a++) - if( strcmp( mReader->getAttributeName( a), pAttr) == 0) + for (int a = 0; a < mReader->getAttributeCount(); a++) + if (strcmp(mReader->getAttributeName(a), pAttr) == 0) return a; return -1; @@ -3160,8 +3262,8 @@ int ColladaParser::TestAttribute( const char* pAttr) const const char* ColladaParser::GetTextContent() { const char* sz = TestTextContent(); - if(!sz) { - ThrowException( "Invalid contents in element \"n\"."); + if (!sz) { + ThrowException("Invalid contents in element \"n\"."); } return sz; } @@ -3171,85 +3273,85 @@ const char* ColladaParser::GetTextContent() const char* ColladaParser::TestTextContent() { // present node should be the beginning of an element - if( mReader->getNodeType() != irr::io::EXN_ELEMENT || mReader->isEmptyElement()) + if (mReader->getNodeType() != irr::io::EXN_ELEMENT || mReader->isEmptyElement()) return NULL; // read contents of the element - if( !mReader->read() ) + if (!mReader->read()) return NULL; - if( mReader->getNodeType() != irr::io::EXN_TEXT && mReader->getNodeType() != irr::io::EXN_CDATA) + if (mReader->getNodeType() != irr::io::EXN_TEXT && mReader->getNodeType() != irr::io::EXN_CDATA) return NULL; // skip leading whitespace const char* text = mReader->getNodeData(); - SkipSpacesAndLineEnd( &text); + SkipSpacesAndLineEnd(&text); return text; } // ------------------------------------------------------------------------------------------------ // Calculates the resulting transformation fromm all the given transform steps -aiMatrix4x4 ColladaParser::CalculateResultTransform( const std::vector& pTransforms) const +aiMatrix4x4 ColladaParser::CalculateResultTransform(const std::vector& pTransforms) const { aiMatrix4x4 res; - for( std::vector::const_iterator it = pTransforms.begin(); it != pTransforms.end(); ++it) + for (std::vector::const_iterator it = pTransforms.begin(); it != pTransforms.end(); ++it) { const Transform& tf = *it; - switch( tf.mType) + switch (tf.mType) { - case TF_LOOKAT: - { - aiVector3D pos( tf.f[0], tf.f[1], tf.f[2]); - aiVector3D dstPos( tf.f[3], tf.f[4], tf.f[5]); - aiVector3D up = aiVector3D( tf.f[6], tf.f[7], tf.f[8]).Normalize(); - aiVector3D dir = aiVector3D( dstPos - pos).Normalize(); - aiVector3D right = (dir ^ up).Normalize(); + case TF_LOOKAT: + { + aiVector3D pos(tf.f[0], tf.f[1], tf.f[2]); + aiVector3D dstPos(tf.f[3], tf.f[4], tf.f[5]); + aiVector3D up = aiVector3D(tf.f[6], tf.f[7], tf.f[8]).Normalize(); + aiVector3D dir = aiVector3D(dstPos - pos).Normalize(); + aiVector3D right = (dir ^ up).Normalize(); - res *= aiMatrix4x4( - right.x, up.x, -dir.x, pos.x, - right.y, up.y, -dir.y, pos.y, - right.z, up.z, -dir.z, pos.z, - 0, 0, 0, 1); - break; - } - case TF_ROTATE: - { - aiMatrix4x4 rot; - ai_real angle = tf.f[3] * ai_real( AI_MATH_PI) / ai_real( 180.0 ); - aiVector3D axis( tf.f[0], tf.f[1], tf.f[2]); - aiMatrix4x4::Rotation( angle, axis, rot); - res *= rot; - break; - } - case TF_TRANSLATE: - { - aiMatrix4x4 trans; - aiMatrix4x4::Translation( aiVector3D( tf.f[0], tf.f[1], tf.f[2]), trans); - res *= trans; - break; - } - case TF_SCALE: - { - aiMatrix4x4 scale( tf.f[0], 0.0f, 0.0f, 0.0f, 0.0f, tf.f[1], 0.0f, 0.0f, 0.0f, 0.0f, tf.f[2], 0.0f, - 0.0f, 0.0f, 0.0f, 1.0f); - res *= scale; - break; - } - case TF_SKEW: - // TODO: (thom) - ai_assert( false); - break; - case TF_MATRIX: - { - aiMatrix4x4 mat( tf.f[0], tf.f[1], tf.f[2], tf.f[3], tf.f[4], tf.f[5], tf.f[6], tf.f[7], - tf.f[8], tf.f[9], tf.f[10], tf.f[11], tf.f[12], tf.f[13], tf.f[14], tf.f[15]); - res *= mat; - break; - } - default: - ai_assert( false); - break; + res *= aiMatrix4x4( + right.x, up.x, -dir.x, pos.x, + right.y, up.y, -dir.y, pos.y, + right.z, up.z, -dir.z, pos.z, + 0, 0, 0, 1); + break; + } + case TF_ROTATE: + { + aiMatrix4x4 rot; + ai_real angle = tf.f[3] * ai_real(AI_MATH_PI) / ai_real(180.0); + aiVector3D axis(tf.f[0], tf.f[1], tf.f[2]); + aiMatrix4x4::Rotation(angle, axis, rot); + res *= rot; + break; + } + case TF_TRANSLATE: + { + aiMatrix4x4 trans; + aiMatrix4x4::Translation(aiVector3D(tf.f[0], tf.f[1], tf.f[2]), trans); + res *= trans; + break; + } + case TF_SCALE: + { + aiMatrix4x4 scale(tf.f[0], 0.0f, 0.0f, 0.0f, 0.0f, tf.f[1], 0.0f, 0.0f, 0.0f, 0.0f, tf.f[2], 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f); + res *= scale; + break; + } + case TF_SKEW: + // TODO: (thom) + ai_assert(false); + break; + case TF_MATRIX: + { + aiMatrix4x4 mat(tf.f[0], tf.f[1], tf.f[2], tf.f[3], tf.f[4], tf.f[5], tf.f[6], tf.f[7], + tf.f[8], tf.f[9], tf.f[10], tf.f[11], tf.f[12], tf.f[13], tf.f[14], tf.f[15]); + res *= mat; + break; + } + default: + ai_assert(false); + break; } } @@ -3258,29 +3360,29 @@ aiMatrix4x4 ColladaParser::CalculateResultTransform( const std::vectorMsIK9a%(Cq>7|$t1&-*@_8Z; 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

Vi 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 =Geb5c%^8 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

>38$c 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

cXK<%%4toD?kC_HmG#_L9 zOedGb`rNS44L@0?E_E_+@ql=hxhAPK^?My_?2VlWs4tl)R%q2hZ2BGNga&l`=^IAAYhGhF{ zuSPa1hooe|*KyRv;g7F2;WogC4!XQHE!XzX0bP)#``26>gVq1E- zUf;=V%Y`PT{Ouvxhn)1P;0ea2+zMe-((F={-l z98vokR;D9T+Ir^ScD@~$(LQO{pY@|Kr_jMpB2Hj+PutkmftmDei0-%VOGR%NR)AK# zbUqc}CxvwCsdmY(L@vyB6N*_b$}IVQvnJeB>wqGoC~XJe7D9RRP^>1aWC}3qQn^b) zyGO5bMUm$7DB9Ycd6zxBHZazUHd%0zC(ufw0k<43hIjcY~ ziczb&5sW=WM^0MsdY~kjsM=+6Emy2Rwl4E`F7NS>ts7jKQlF@(i9(}gM!N#S6uSnTl}tOea^P$PDF;$0 zBdjX0P=$##X>+}XSf0EuuER%@@v?%^F+O6?H-hIXK%?IEoM?m@;fNO} z%9Z|xJQY(q{;Ig3f%y%)TG@t|fy#!ToLzT1PR92Gq5v@Ecj_3bkGTG2cXeNT+mU6EveW6Z4q0&9I*XiLBBZwF)!`i1yYtYn6wrIKW zFhhQ%64pqmYeSRm495HxUF2;2mRx7k{M|YENMSS2)T+!Y#|sV#Jh`hfz^E<3%Vw0O z$t{7QZ%oSbIxgvw-O<0sci6>UOrhd9DS+C^sKzwJ#}&Khs2;F9-oS*YHw!rYOg|8o zc(aXIx5Y4+8TX}pJ*3##CW9TGw-cP#osLAK($JwpgMU|Z*T0_JEGb3&H9x835%(cV z6v=}};32ZkX`IP@*FMc^v!skq7H;D==#2F12zVcnZ+z(!hq@oHB z5M7)nttL0=+fG*Q3(wRAd>EdGrK?IyBGO)zz3JS@B%bc|*7Bv3RHi2(H^H=n&Uo&> zHCLAzk62T!_0~2nliyA>z|4#Mq=O%ZD0j2SfxXRi`nv5%R|QEv7&6}*SK{}RF{#!F zbhcwJ;UHpP7f*J0p#gQ`6>{hkW`1NLIY3!+QdVA!3zU{nF zO7>IJ>&?!HaCa8K<-?oQquE=`b8Q1cLcy)OqV0$>lcXk$d$UE==Lqo0KbRP%W|k^*8o-wBjn;)u#95l0<9lE#9WV7>TlMXGi=~J2yL>3^|9@$`4(OaBL{I*%m#% zI<@cABik7Y8K7V@um(pDFeQ1tzN~|Cs1pC_qxi7s`nX!>?|gllJuH5ES$`zQ1Z>C58xv;*z0c}4De-nq=;`}Ov=9?btMChM^T-1q5~|IPaO z*arE@3y{q%-{C=kJ%KrUD`~Bwh>Z9n&=>47Q`r|xo)9;>b z11QJu<^AV)6*u|6S8jOvzJx))}Q0uBK}{uUKVe_sLQTb z+%I7Kpp3XxS$rRtUB8|VR*5_Rd<4tcbVK|f06jp$zyJKON8^{D{_*{verVtJr|&*~ z`n3G|#jxygN5nI=8A zFZn0YU)E;bzPc^j@vD~SJI&aAb#fwWo#1o7xK>}k>ra#6bb9R!`nm`8ude*sWS0|XQR000O8jZo}Psvpp3+7{4FCWD zWOZX}dSPWlO)hY5XH0tqSX)7}b`l(lQ{0LdcXusr#oeLB-4m?1Lvi;aE$(i`wODbN z0>z!&^t<=}@6D4uvwJoDOJV%e&Apr8PO==KJlb(%qd$D15KFA(sIsa8>?3L*t|7b zZ&@v}XuwS77lX;q)+*=q?JDh)ssPp3K2r&U5$BwJLL6O5oc9q4>T(ELx)Iag63wZU zpExj~%F{Eha5@Eadz0ggsW@{Jg6=l>R$U4FAS3)FEyLhG9?~UQ6H9_F> zR8Q){#bRd9D*aJx9KI;%OH)wvNgUldFioiEXEG~M67_=}>LAtMK-rBHq%TfEK|IaP zx1uT(5N3}?e>hT75H9Vg@twDqiI)>J*Y55PPJ6{Q`&WUiB+M)*&L8yL0036%e;=TC ziI^wsB9gnjvJBD=(i;?b0;TNVwg3PnKwetnqxZ_6PQR3ozcP4V|7iZ=Ya_AEtb`EY zGvd5WN{I1}-QuF&VMjG)r;3x6n>QSfdDvHs;j+G@&D>CmnUUw_Cw?jPCg6?%+bfDe)pN2Aikn{FH zWU?8fT{AqawE|$q^P!PS(l8|{@?}@*eJ;Xi7y`OqC06q1aaGRZ* zB0oLeEk^#4*u|i%N6I+;`A2r2+OmoIH=^h!a?ih;C(#(=eu|Shy(kw#Y2;G#dowH7 zOM+sJ(&&xYieV`#4MjTK2<{TghZ?=CQUq<> zEcry)Pj56V)b1{XY2TP`N?muyu5T0`pkF&kdxBH5w2(G>%B}C#1zfg^4JlJ(*O|h3eB_w!@*&>Iy22E9V?u4m>z=vw3hdA?#90<%fFbK;m7pIi%_47 zXY~gJbW7doyF=-HO_0vL;5P?gn#gL1At|Fxdy^lF8QVoh2wcCCuT2$w=JDnG@6p4} zET+%D5au?C!nO(irFGl+SCljmAt%1*$;a~cf4-793YCvB3@()^7x=c~=&&i}UM)S0 zdfj#O-(%%J>y07HBDLf1P5teG*DZXe6EXa2(UXiE05&E>``ZOa!ZEL$#8X7ZcWWYfPdzEF17>ezv5F*l6NsV-_tg1MArNop$9CY{e<24tL4CQ`Xz`C>aSY_ z-4s-M6)5dUc=mAqr&#h&4KEzJmp7V$K|^9*(HCN~_Xp{hXiASn$0`bccmxU5?E7BE z?Clg`rlhOjuzf(41!Gs#I&$>Uwo@R1V{Y3MMH0$STsZ(F3{!p1s~AE^Dq92cg~dMD z`cQ0-`c;MU5+~f1uGq=cNGiYqXjbQ-^_=3%tNlx{@Shl!>=dIb<)oJ}M= zVLH+#Tw5ynVmNW$p1yN$!4bzE)hSe&AVcDRU(G!^Ja1w?pXx|! zpAOP7KW3ZZ-@q0}!LI)>{GAhRC^9)c^jE=+wvut=3Xvm6mHg=&SaHE%Y>C*35Ua=8 zIkUY7l6+sh|AF*^e<%>GYMteagLU=rlG9eiu)13@H6T24lGr(k-Hsm@Hln3ys|o`Q z$^@pPY}3xA)BwL}G5f&`kc$0Cc_qh#-aPraHkD_E2zheeV0k4O@bKhbSU%%YxKGRm zm0v!pnEe)NzTN%q*Y-l;N5%EvNa&YZD8UB-P{OvpPmri6R81(M2 z{%YpR^}lzd#d(KbW;%N1BDaaF3J~s#GaN-KX`o^L0wyaJij+DQ{W1cLTgh;wO_Cnr z;&|9lzpFKq2MnP)EngUYW~*mf+>4PGnX@c)LjQa%hLQ}9TRDP#cNjRw@o;bvn<7sG zOz#EKQ|O`D#JWVaq=mu~(1JsCt3_~KMaER2b*4dNsKNxdQXntgPF2(~z+IlUbihCI z)Ati>4dWPmlGKx@f?rG{N>fKg)e2TV_Nf}N|;_GH;>viIt# zl9E>MKUFeEmnE6*eITu!^$BGB@-1H1uG2~_3Aslpk#A`THR`QHMm?WM>0$bH`_H3( zca3~l{G%VHs9&XyoA*T zW(!`Yachz%dJa{6{-3KTG9>9lmklp;Q6{ZS)}EI0--5brAjfpMr;^L>ZGN>s(D@xK zwkIwN+TAu>beF;&rjob>*7VP?0e?_ui0@o~OAn$cT09#af-yUtv|A9ckRFkK zd9rHzwW8h$^h*EL1mRjx46d4ilnCD6xZYcQ#W(rP zs&C2Ekc<>Ce4ytf(|uNLI*iGsW98z=v+G#HAO`@bMMFlQ7~f~Ar9BjCNQ9s`;o$1_ zje^NKT>$=!Sc4_Xw|vLLycAznggPW;psL4S5`a_F?plBSDt}O%TPS^IZWky-X$#h^ z#m=$44;-?1j{gRDacecSu{pRs{q1$O+WL91hD^xQ|9VEw!NKA5^puQ@EGH*NL`0;s zv-9QUW!WBIoH6xVb#?Xk?@qS1wvLYE0Ue|{(42dCtsIx(bhWjY!Up!co` zNGM|rnux)3O(6AtRF&KHKpE2MV1JU2wtExBn2pVT)}%`^IvjN`EC;!D**P=jx*8G;d5MVfi);+@j5J2>2cXi3U7tt(y8VeCi<)0am2Kvw||%a zffn)kRetyMJ$L!Y<8)rMh!_?q4~w_^y4U+MwBSXDBv{&&koriEVk=KwsZ_GF!m! z_RpNE*b8pn{lN9n98=62Poq*TBIojYOL)^iw?X-SK@=}C&TKXaEcf{-%pX=4Brr_5 zokWxCqQP*gs26H)ZDvAn)!u#&%am4g0jy1xmaJjgfXW4M*{>`HIDR=?*5mw7f9eUCb)8r2%3?_c zIA<_BchvvrC$WR_E1@!@e(EQ+h2(<;M^LO#|5lZL^-`2G^=uqwEG{)1h;T`Gq=*2y z1X4JiTyTQvXdkJBYf&f>ODQ!ZnA;?nI`_k$U)3hvfxo_KO^HFD_b0NLY}IWhvwhZm z4p~P|VnxFk_&1aZ*RqA{5ikC z?3XY9_G5~@-tWL1yfV`&m)BvXB+a0pOu=77Lb{b~SvYEJdcv0GvA&2PmDtD6a*gwAaCC_(?%r1&hDE&|Q%D3;Z-Y zf#zZ`56!OYy8qS0-dLL0)i@Ut25I@|4LQu1VFbo4_RDpuftShht#s=lr&rVUq8IRL z^SSSKo;Iqc$Y^FCX_d3kA-TwhE~Zg6V+O`%Slq}R;$2uTph6_Es2I#C9abbQ-jwR9 z0yC;AM9l@$fsRmdXnf#xQCVJ~Ac~%di=w9HoD6}{bqQO*`}f?=-YlNuNE8edK-+?D zle@d&KsU2QGhbI5C4>^@*CKhr5RKV~5T%~$6U0C3I@mz=)J#=NAvr)Gkq-jhOLv{g zVEcKO-=dM1$mLIJSP<*~^Z@gJw(jRk5~q%f`<-|;=&wJIpWUCCec-WHdgz6?*Q0+N zvLZa9N5v*jTgnn?rZU{C>A}68ijaXewUarK(6P_1KoZ0m&hYt675JnN#Gl5F;@6v< zHsZn0JAUV#Ugztbgf*Oj&-W+ayI-SCp3O6)23`#QmT4wBq+y94pDY%3)hHe2jh#Hn z1cH&RxAyzQO)8rdnZS9esCwy?(xligQYeth?DH8XZ55e9OEraTz2;EwmI~;erXkwOBLewY9Z>>SXeKmW?M5n3doA+?zujZHVL>Nd4$%0@nM|cL1-F zYq-9ahjwz2fVPCbit|YQx*2p4_O@aQ+wT#HnnB>Q%I5(wMqIzJeSgmwdYo|mG3HgF zRpm8Y?0KsXjm@yma^$yD_bK^ce#gu0Z^f_7t$+K_pnt=~QckI4!7|3E#`+)z*B#1n zjGqW?e)|LbgLYD%eL}HytTo>j@dpPJapva7!c!N|iL2og_{PU{(2Z7U%PI<3mFCFh z;K`5aB;=|g@=1ZAIbQ1muwb6bq|+9Tl>!r!S$yY{Lg#a0 z`nL7E+bs0rXOhFGiy+q#;WsJc0SXwR1kNb%D(Q#h?-ZKb3!NM2tw1I9V-H|d4L{O2 zf3k|NvrMgE=pDG*dV(0~S|LLW{3-^$63g@6jw}&%X(9s)BD|a`lXTY`}Ywk2UPInG}XfR-fo#in#H+B^uGK^)&%a{e<6Pd?$AJp`m{;=(L-FiQZid|eYyVA*ph|joliW8*BJo?&1uHIC zs?nKGZXhJ~98?3;0WQgKb%ATQvJL|cs2nTH9?I{P6a-M%6P#N8tI!Hl1(0zy;9fgL%iB zlhHmOQDYj(eq67n_I6LrLyjI@&!c467q;IODawh>1Dts{j zyU&LA4tn^G`fe=WdOB>laaB`(7@d?eNzMgrHv{g~t_k)Ye$JRIVrokeuM)xQ%?f%U?zFEDR@@B*8{XM6YkGxFH&#*pjoBD`nN#=278Y(fB!^vVT z3~TilB!M<@B5MKvm@i-Ir=1n5Z0>g`pe@c@eJ0SyMU0}Ium2<(0qZmDGU1hw8!GW6 zBMK02;%fBE%}pr((pdGQrM*=O@o?x~7un<5#gA(>UwdAG$419AFg_fMR$ELP)dI;n z44*w5W`kcJI8>D^WCSdDk;UPe1c0jtpiVIf=a(x#i+<0JUZ z8DZfjU8LES%4yGMX`wHJKlU$e<&-d+T}$?DxnE^g+n@$coqNu$DVGZdPJ@m}Fhct& zE^Y(#xBXQH_qS9h^kTx}J;&qNL4EW`zcz^?Zq1an;tXpqq|$!b-en#o_7WW4pH3}d z(OLM2T!Qu^ab!ZQUH9hUuY22&Xtpl?A#d?-Zb0Bj==3PDpv%;b@;Z<-Z z8=}TnWJsjC5b|9CIJMz@6BD8{(cl)G-At7>Ha>FBa#iuCs#0{5q+v%h55m?haT^ZrMNPekLLoB`ZUqboxUo&05{c z>M{p8TYBGmP=ObUk@XGQ`h(`$vQW%QF_HvH%J1CPh>_lD$bsg@?TFGG z>k%X`Y=oS3F~*&Rrq?jbsM8_KokK7j?>IkcQyGLAY@=)Q6 zP#tiAq7wM%|2|Xh2OCqhr1S7-E=sWDKtWQN$StNWBDR%1JXOClG9@ zMns!3_uXbk`kBRd+;b^7cBTz`WM5_*dLG~LIjmf#e)qRBHSMW(oV7GH-DEO(7=;lA zbKgY!=8)UZk20HQPVgz*GlOah)@3Fmd$%2`DSZ|-MA{rp`jU&%aj&laX@0ZW-qh3g z-G9jIs;f^kKi4eR+r%5JS&rt4_|>%T6F+fT5<>~DWThzqtaK1HOjQ-q<0`eSS%v+4 zP0`W6xaNIb6oNkR+gsbc7dT9zGtaz*9r+B(oL?g6Cu+~2508u8XGx;CtR%el_pq|# zLw+NDG4TQHcH7Q6@NL~%36E?W>t~dBtP6*clN5 zYmMH2dy&q&p@GdcBWX{tLN5|U1DgL)VxGl+7F0>_A>CI~;`vA5p*xat1+XF!-1I3s zZYS?jy3Q_`hoOpvyL__&@hzTQ#_TWZOT3I6uXF*3^R6KGc%w(lL>3?Twm#2eGEX59 z1()vmSf@Kvz^$|z&bobJH~|6AJ#yFmTc?5@ZTivcIK%H8j1$9yA8mYn{X^j#$~)rQ zAjVJLWu#2|Jx2Z3c|mP@O&ALs8{j!*K7s4+l(6xfQs!H;bfJsa0e8Fy+CJb0M@*Hx z3k~fN`8Zl->9@v;cDWl+U2CXsWik|gW6@cLn#v`i2RS*(r$q-NI)zq;thr5zuyy$-EH8o3>GT6-| zVT@Qx3J#K+n|phED*|g?s@USDajn;VKHxjWyWIJs$PYU;WiWzr^<5cXpr2mO4s z2H?HwWFVx7gQgLR7y`1Y^SkkZ-VKCR3{!IE^b#JHdoOL$`Q~l`@NvGljai7q=sFhe-a6w0_AOy+P}p}KsBG5IPOOWGSvh0*%h?^(YQ2n>I??kNlv z2EM~arbJ#qh?v?1vrdcisu8KxJ+fJp3CyYMR9~xI`JeP$vuQ+MJ*S_|ND|{xEqPw zd(#AZW8!tXVs)osGoF62^7(sXLxVI8posE^wo`Utr*}5BN#Rpi!?al2L76Q=!}P|V zuNmA3nw%W6%e^eu5W{Zb<)FZF?rg_BcCU?qW+|xJ157*UtRh>$4#~!kuvfrPJOwn? z$$tl?N1daq!*bo;S^Iet;0+x~I|qlCGq3J;SWMTQs`G!i zbPBx6!YAl%Y`nQyG|3YU@Eu_RC}4@ycI|L=8E{bh=n$jn&U`C`hJK7F)hK;!^No<18cw7HIj(gp zE>pwH)%^wva~xMRy@aE=GO>rfK#&zn6lssAiRV5is60(^bm+soLz9PB(?EmoO7}-tsOR5EoVFWnl0->MheMBgW|c z#ZLEC&NAHRtOo?nuqBHJH3%GRc|X;d(gwL2o^01L%}lq$WBYKu4sbJYHTTM zyKm8ip)eP#n89f)&+pHd-1Z@WBG2!v6;teax_Ud`|EAKU@ohjO)m)h>6qZ+rU-&5k z@3&y_Tt^hNX(rwS->n*~YbV@O+;2~Wew`6}^@;3j;Q8utdx2kw3L<_S|AMu4V577-Fq5Xr5%*&Np$%#hov6{O85YH zvnkU=b@MCm_H^)sD?{r?8$LpL+0fVY%^fYYq)@ofJ4A^_W*5>1Zm76Fkm#n}l^R4;H9%a&q=y z7z9oHGDk%9-4O-JYO60y z3+!_atHQ1ka{<*vZ>DF|S7$d+vdbA691W;(?YG1sCmatW6SQj~tFl8=@)4|7Z}$XpR0Z2=W(CEaU( z;Y~$OgV^E2CMPEwKQFf3kfgv6fNHL=z=#gN&*koNJxjy0LAz)5R{~hh+yo+EaSX#X zkP?sIimdd#Z1&eK$%JTRVCxrJF%dPyOaPhE0d$$C%`n=ra|`%IUk~H{bOaBtPv@Wa zKt8mcgPga)%_o-_djbXoLq$gHb52}3pr*N4L?NP-Yxw#XWB&`8$u(C@A<$-a_x*OX z3y!$EQ5Gj}eoRDYc>Mo63-IXeABw$)%-k2lWmLn%CDbyR?CJ@)fg`i~p~;%&_pq0? zw*A&}H06E&0hLMMgO#t0u5dMFl%PW055#Lvb;>vwR@dKyOY)NEjU#=5MZb?(3(wfv zldrL6Svt&l=Pj>09jQ)zsAKFiSet&AD z`f{y>9105no2tIxgIOmfrS{sKIVbH}UcZNag6l(lxl|tP_4SQ;NP2$^<&_dJacB5=p*YMg5g()DC8}j-5S{;5oNzz#B0K* zwh!B%O?eQpGMOn90L(x=sU&}Y7I5d+tMFux<3qI?50e=ZaNVqE*Ooc3_}ci>q}cr? zT(iAQ9Sk9Dc_vc${{_-wkQxv(kPrhBOiPGf=?cC>YU&NpNL$%9QgAbWP{}JdwcZOF z8BFt+6czPjL#QDVpu@w%h*$u_{Wi*v_jZ9OD(!?Q=o(gq?UOP)nbm5dYjwZ` zI9F#$KZ82`ET@gGfT>)Oe+upl#R_?yDP6vWrRh1x%QF~9X>!^ymP7+cU6deG>%JjJ z*-3fZhDu?T#ZWa=lC%HDp01=h>a%gyVbUQkQx2JUQVeU{_1#^&7E{v8(_Q!N%4hkm zzJJLC`uc*8xwNsNx+Sa$qVyHheIs(n!uW(zE91@Wtg~)(M8CxFefS}`e9B$?^VM3r zS33_{s#c{odh=LO#~W=U@!o4NHWIrcim*28Z+wlkH*T%_t&*|i?IN zpWUKiXW0?%5#!me#9qBF$Jxf?`<3%5BIlrII$Wb(&KnuFx7=NV9e~v^abF``@ zqT$FKU#>rAm!g&P@=ip0E0n`2R%V> z|4K@7amfIh#3{A%o4!)`lS$eV17dAUUOV`SVWor0 zmpn!B-_8{b>=hgRy4)LUcKD4yocNwi>J7g5^jf^QQTkizkkoH~Q$HVf3_0*fDFYp! z;#x|ECs;b!C$VX$Au81qm}G*x3rbXC&s&&B{Td3C2F;EyTbN+Nxn9_P?5im|*keqC z(tqfM*se`x)^3}k#O0r0Z)Z-{-ZQ^7U)t~q3Wd){I&{Q{F}yun^BZGnh9z;w*1fOy zk9g|Fh-}9MtPjcb_m?4#RCHl97NK>>Ooj*R6AKgZD+`UUNs%s$-*zC*pu=gHK9A&S zI=SB4k+9UkhN9!T5pXARF@SqPmUqeNecOF2S&%?KiAK*35*3{c4K@e$tV=reTlGB*@FqnkCveaS(fOsq0CTkPq97;u#|G&ZsrwKc>j zCyU;1hLk9#H8nO4`r?m{Z7#!H;gyuFik7tR5**PBIAQO#3*RfPG(&V|CKqRZ;fUsh zkRjRPLX3UA>5^SO%O+MEG#k5)&{|bw{q72QqD?c@2-fA^v-+!W{StnQ+Gs#nrKrC; z%i`uYwkHQJt&pHcRYGe@l!BI1T!=PvbBugJC&6dY$J0bD{qWE7wljq=XhCC%y*&al z@9JkF0Y(<~UcBEy2`4B2z4IfG%2EE#i>d+O`Pn1nL)o0;L<7I+>+{3O;&--*qlND# zV&K>3qyn)$tB78NPg3QQ~k@SzBsLn9}z=6aFW%+~CEtH<;~Z6bPFA=pZf1*H+- zNnN@1zNVtW<#Q?RdFS1FFanx_8s$GHL_ovuxLfz5ZRdfF&7dC&a}O}Z&Kyy{qc?7- zKd?1c+g43kzTWZhV>l1jD$QbSjI3YY%Yl%^QxjoS$Lgxp^b-2}XBcmMr2^U*j0qLeS&i;WE^(67*Y& z3IU{^09ODye2?UP&}xI?Tj8q3*j>IDe^M036-J*q+Am49Rnb7{v~h}~-@H6Ltq5FS zjoLle{I4cp9CiBtlGjiFOI|-J29)?CF)gF<3nD;z z@blKC<9IpzWClyAT@%+1ahLLZ>|LT9XHDsdkCvt?mU>IN^!0XRaZQ#oz zjMLP>IsgmLd!5*$V+l>SM^Q0=UF-|!#g$*p=!+2P-s;Cd_iMVu1 zwsv-Qwzh!jxw*OZ^)6U5nUH74&4NJ|ufs}@>;iD zlMS2cw80QlnRLGEw8B@c+U<{jne~%fTQFL}9fv+D-yW?+jUrcR3+@R(<3Df;v5T{4 z7YZ?Df9zB!Gi9?ObQkc)3*zh_=QvT1&3vrFr9 zhOt+qxM47WPOp-Es67VzSwt(=I4*y%-q^=gNwHv#H9b&!6d4a}6Cq;-;kjQEJzp!{7=5R@kg^%&AexbHe&h z0uR4dcn~OWiffnfe;k*9(Y!Hdj`P^ff(_W|Vb)-1MfP3b^ia43qBQkB`6vm+S6NcwK;uCjgfo zD!%z>LpzRE2*6)__lA}H3$*|1rYbg&P z5nX<$RZ<74hJSD(z0qAh%*+Gf7CT%Jc-%PE$mF4ia^zz?-+cH^ixa>E5T}0#H2TQD?#D#?cgO??6Ec#fH@Yo{|0BOU{ z{&XsZ2eT%w#J-FtQL&K&l-@n=`Zogale*9k!QpjV0teYrXmElRjAk)8Zj%DV%1;|3 zo=$%i0)mWnMdN?+Q5}&e)WQ`WfLzGru=oViX6ZI%4+_Cg>T1Cnd;}$0FbqO9LMF>+ z$Nei?z%5SXRu|?m$}qVk;I=#aCmOFd092BT zjB9aB`s(9COS6uuwsy#6=akW3;R>`w!x!&Oet6dX}F!l31MgyL6 zz#ZcI@$@ZV(tsDf&K0rBu*=&36A;M#2 z_ajrYe!p@#W+#;Fx<>8@wO4YBTE-R?Zy-Q5>{TX&p1Myt0j|MNsVV7`aFDVZ$RKo5 zHrv19uzu;wcY<;fqbW^Z;nRk>II&l6n1*sNc|WHj6kGZ#7H{a4!3?3&q~yUH)-)3d z#uE1|{5?GSQKnE#Itg>MsL=pRd90whW{2C?TJQ@f00+VaV)hHt*0{9H%F4>YW?!8L zXF}=2Yf=$JI;u1_0o`A+G#X#tj}mj*ZV}Onj)xz2Tz8Ec+UOCo(AisQ1hZ26CLV~o zk}UGtmmD-7KCq2`Auw`R7tHZQ<0YlZQ896!k!iR)=$^j%ZHd*BkH???myOIcY*S2C zpT<51jwX)4cg{lLHm)qgn8?L5m=#}>s&h0knN@-#$#Uia&#E0}y6nKmBbdjA{FC#~ zd6Q+R)q|bIhFt{ZC?6hPeatzaMXEHlyEEgTo~G;4q&_QnYV`?ttR4T z8Ru*LY^vyHoHA&0uP~_IqEt*{rTqoA`aRt^8sEN$KZ4BeWLu>>FB%cqXjuqaSwpooN(NDKYmQN5u;tWkl_Hx zWAT(S)b_fh-A6Q*?s4V{-W%^Dk;@e>G<5TKP zk(`OoUTTBazQ=Wk5;Z`+KZIX&ak@59=BM#I^9ouPCq*XNM@vC?0m{-dj8WNwYrN8k zS}V439F50B;SFBAMX~6IL1)h00?f_nkQDTPOZm}kT}v#wjiWF&rsVMCawzc^SclL9 z2bBibl_+z)MPiqDlg~i7DgW|T$;Os|uMK+-b%~fuTug@DZxnY)DR(~By3#8_>h^H9 z5eJS!jkja#3kfj^$y%^h9yc%Vx^q8zu`-eT}DEYL~9P4vm&wKC(LosA)hxpjAX!s zuG6OkhcbKbIc2_?B!rxgkcOtFb@w@C?wfA1@H4cR=ur^EA_3vbZ@pbIR96Un4V$LQ z9y*3BfeWPk8|1`WF=BcIj!}S~BK#=775a;vA#C&_6(=RP|TvSUv-Jh%Rr*TaGnYZEkr5@kK5ert0-o^Gm5aJ5|6_8`6DK8 z`kbrTiFQ<&HgVQl#PH)8_wMfUeZ5L%fi?y5aT4o9697F9_2x8O3c7*5zGLT&cC%?J zYxBA<6Uqya)K|RtHG~-6x?$b2$yPiih*ig1gu?qR>VR+&zBQFdCT(09Y0MdkOG&!Y4c$i zQ9PSjdP{%`iJgogZ4UDDh2`S?d#>mf{^EyMHGoVKl!5 zp;8(~^9`&gv*lMBz-SojOdHbm7~*)C481mNau3szK(=YK`J#z9o&A{6%ACEMhNPLU zQTFfB@%tcOWW?DJojsIS%f&Bq0kqi&`Ao~64Qnc^w_ivc`R`WU)-tueX8WDB&JSS% zRvdNqkxmGLBe^wI4xRdo3S`-t=5gaukte?vgu=h^C3DN<`!d^{f!5$5|C%IRR1pDp zKOLjNmVbYRT>S$*x9FKK4)CVtFIe(D1^cye1U!BZRuyzZ$`4 zN`pueZe~B?)iM)^2Z|@DWT&+}vL@7%KKl?}hu6iGi}7lTkjZ-c&pc$A%UA_8lBpcd z){`Tp;Wl?3f2;g=G@#jfB<(Y-0IL-`1~?1&E+luUd%V*ve-oH7;3TSmCSNm~hP}TB z$=9t?!~066PlxE5q06v2+9`sFpgbDPIm2oUx!Qd*`tcV1O7E?^Ma1^yEDyYqI-!xq zj?1p#e3I`#b7Nx;%myU9_ReEpbu+kZCeheB#FbZ%!4zOYvgOWEbh|K9oUx$v-N}($ zbT~3DZge~lg^F3rQa%}$KBX*09Ye5%x_Nw%Y?YRjILZu@rjCp(;^O=JA41Nt2k607 z^@z4fYtb+BGl1f*IYA(2*(Qz4vcH?2vtET--@OUBPbg2=(RcfA~SPT_NIi9R|&i^G{ z?kDG4hG^z5(+<0$410*V-#B}gPGnrrC4TK@RHk#f-=y2+sYg$(xIm;`!xPqG_t7G= z@?3_9{BHla^g?!jdMp~FK2CVL0&s=OnAc+w$;Pr9Bi zM34G}Aj~nu;ZlR`F0LnysHFdEd;l?OHFfv=>#-NQ6&(0x7>CeIYm933A(#OpXgET7 z8*%Sx1Nzz*hPGBmD}m2yc>i(^Hn9ko!pz^{SgV)LMgBVK2KiY(Uh?Ah#}$HHEqS#O zf-kOqr^d$au$0_K(b=6)lf?9mbRnu)3QGr>ll#IamCT>nAZOVybaJgM3nOXK6^O-g-$4s~Urt5{mlSo$8$84LO8a!;H zN+ZJ`rI@$7aHdlOa-~9sm`!mb)I{lXM*&5wb8&E^w9B1KPqe#jky-I1d}oPrPk(g=9op@x~ zBVPKb0h<|!HaU{G8-N%;G{!;q(*;sX`pUEN8rk)b@?P`b4pp+M=yJ8;(cW&dYT$82 zS*cP+VXf+Kv8}(~Csb_g7&?&VKVVDme)?gBs@7J9z+@psCYQ^`_i_6nlHZ=PP@?CC zF9B-2Yd-q$dI5N(mnKDG*z3WZet-|SDvB5N{q;>N4o)HdwNkrU@bR#S=k~}SgcOK{ z(8@BBf5u!vQpm?&-v=znY}xn?=iGvYbrjQPHd%>AmAXM0eL6cp6z-Ox?ATa zuC11H1`nuPdeXXn3F_)^u$#;EI}ao=SCDJx-x2fEjrEs&qdyw7WHOH>KD8EmIS>_!UYm0JXg}QM_4-t?Pf{IPH%MdK_H9M}g z)zq*yhhkf84@APIV)yIgN#orgUwpqBr1NG;fXg>|ay33~=JEb7c)|EYtdEs%F^~(* z6o?7``g09RUPb6q>is;YsYTO>@yzN)xR!I2Q4iWYBqaZGo}`&B!V|q+*jqPv)OTie zs8`m$u{{{QSfzJWp&BsCVutrHUnbgUJ&`#WO>n|>C-}$sOEwQEZdt`zoFxG#<~@P3 zc_KaaEUpnD&@BX-h12Yh>~U_Vjf-ux+{vH6cY01_^3>Z*Ei5iUAx4VhjDcg) zfDjF?2A$BF_Yh=FctT}4R?wM5gj`R!W&==DK!frxH3!)w^0#Lt9`&(2DDL%EL_{pM zESWSjkcopqh+3Q#4%{afJ>9M4)eTEx|Ni>$w+AknPT95cLz;Xtod~Q38`j12GmV7T z>U&#k#8=BTW!!G>ZDI}Nuau!?xsySJ;nQ-a7806`H#He>g=Jp8#gnUV*qU_%8-<^8#*{DV{fxh;zoi zeaD#NXaquewo-`9CAr@RVr##-S%PxDS1YH4;wqq2j zOniDE4}0YUmQa%Gg*8=k{4B{n_E!e;QuOGKAe@(sF(ld#o@^N32P3B5@yL=$XjHVi z9l)krD=gpn9OQVd>f?_T%0!d<|Luj*Xt7iUqD)(gocONhotCEbdPvj;9zYyv8t4w} z?18lC>k^^lyAN|iNa&`cMK(wu)ZX0mB}Gu)fDpht6%pYmb?%Z=gD8iX56(@AY8PdDZ$~m|S9!xOsR$kwBM&W`%nl2r zADaH>9AqWF7yeHW+yjNHlv(HSofc-{CnHgV6ytYFc{U`d(TGcUyYJ-L_6#>wWdu@k zugx2hOj?)jk=yL4BL4qbfCs_}-b;{+wZRp0wJyr_C8*|`rKbiBkDSGco~ z^ywlV?0?>Qced8|U8 zLvR*%f;(X$xCECFT!OoWpdmN}f&`b#?_c%4y;twnt@~kW&UE*j?$g!NQgixVfrc*%OP^B_|a?6pk%|ae1@V z?k)9CI@k|WD|3oQCz5MS2|}U*5&zpQPgOY0godZ}8(~Syu91GHVfdA-($)Evx$8)} zYyO28m?xRwdk}ifMtqmIOV{o0`Oa8@?`}2(8SC|q%70sS{%dP#amq&AM^;?1?0hI(w6b&0CbPLy+|zcaYV9xe##Fmc@TzdC7cfq zZ`m#td(h>Y@T>wES%T{N#9o1UGPDieu6ENb;5{n6RI$D{zoMvvAI{#dwCGKotK|y) znN{UBYxg{eC*|j$;95xxd?t^gVp;xogaakoNAf!_TvHn%ULijuR@t$|Ccty?xvd7F z>g+`_iR^&wPmL1^d~+$^dEUzfV_n$jDIClhSh*qKXp>D24BTt^cch2?S!s3ocewDM zdGDkf@MWt-a4C!BOJ0ogXL1-dSoDG&5pf<*t*)E*eM{1WWtd?7P+O|Y#*%ssyA7eS zZ!w17{!-;9`(4UtWUA zH#DCVK5w|(F(P+SUzRw6KEiGMcwEZuY ze^J2wWtr64>Z;Q-#iL2x_33nHsp&Nf6O6(YXa6d2{Of>E5Q~brnWUX53LZlS63?Zu z92Sncd**R@wPr@anC|=3@`JyK%3t+y^cf{c-*$&-8+`Y?G39FYI$GR#Jo#|6ShLdR z_Ir0CcV%To)!T!Ulk-L2c@q9t2W16MF`5BZcb5mBh*wrT_Gyp15wr7c01Bc)tJ&R{91&G5FkBzXy>EDh~Oj zyfF6IoBXiuYx}aa|9%txPlN5s)t{B6CEFnro&WC+TMGL9XY8IQ-1}!-O6<%>-bh?1 z^)wO&#^Y{d`bN4<-Dow)4#fj46QFN{TJCLSbIRZIO?b!`d@3EsvYK`H#USoU3`8qC zBq|)W@*LKxdiR-fEiEs94ZOd~fA!(yXS3r_A|-X;rM8KQiNC+UqN1X$t*xe}ri+V9 zaPafy{MKtWy%&_Z`Ns70^y;d(`}S}~M#f8Ir%x2I8ckz;iR?MzzUT85y1BWzZxtVC z;rGweS2c#@;&L=FDmdrqWt8S)ZD}u=U=Iqso?;OwM)S8bur7UTGOSrx@}yfDmhO0v z_slFh+ksN^9Nli@%s4;Wiu!&-VQ+NXbVdt|QWPxxSSwip$qBi0-)oAdkW1SAFlqwf zWd#v5lUUsu2bbW$Q~^%hu|(-H1D?A$r39}yT(0Fuk(;+o8wpMg4uydT)9A;L22cX4 zK_{oL1RU3Hd?`~|oX180oh7_*OrRZ;fh`TlyEE5GIAn;w-LQKS&+=%$?QX^pqwWt0 z=FRFfl>wJSuE5!KQ*DQ3m8IvD`-iZf06vMV<)x^R3g;@Sx7&3;iHCUjmC`))gi)>KWcMl zXl;a|Y^q2AJ!6tgnuro)K#o|uHaO0snMIP$_g(TDat;fY>!7)^50d(x zo%uj-u>8^m;V*H~dDM)1O`ha0qp8=unenI&u;d<;Ie%N7qoFNzw?1i+nlEj$g$4Iy zF|U-P1wMsok4=kEuSYv{O{`ja*7g?e><86{e)*{gcsALL+{~-eVAvrjpaBt|s7D0- zC5M`rx&=21iJdC?K=bb9+lLxghbAS=SHPR_xzag7G{DXfe!l!i0>C#Ybu9K7Q zs4;4fGcG*`7Wfo(_tC((PBLnJ-+3G(9FA z)iCd441S(GUG8UrVRGY}j9C4F&0>x|2kl@qvS*5f3$1I^x*UX~d&vGhW&2Xmz0}OT zm`|{mI0y`?v&$+Xyx0bFrn}Q=ypkt4BTmd-^C{|JNsBo|DJFCSAq=SlMXqTEr3V8V zPF=C~^Aj@t_9SOU^Hb7(?mAo?jrg79t~UnzzJ!6fJ^Xqtf1|{D-MP0LVQHhKcq)e5 zAM=c4T%8X@q$?0}U`Q*nF{(%=+87Z<%Nc2ei6`f1H3cXUL5hGLO+|fqZbGDH;nQ5X z1g@ZB`aItzgyaV2(dF}X>%K1}kUV+Kd@$}0JuJG;Mj|TQ@#)i-@5a5;3UE;=Ked?q zm?DsgW^5ELC%FU5ekpDg(FCGm7Vp8ym!~R+0Y{mM`f>>>FXlpTB!`dEcPu{iHD0-! zq}yUiw>Xc~@L=@2C)m>m-oKLWX8qP7>Zq^-XiWB-alP*OKF{Bjj5^ryDesU;$q{&G zV7!;)Z6CHjV`Yh?XnR(eK1aAAXw~#NBss|YvCS&dNZ=k+C~3lkkt$~X=KK;25ri#- zEx<|um~Ag|kAJU!(vi#;{N=D=O6WUmzLn(VgmJ}TC&d=b01KZl`-PKmsaz3qePZW$ z3GAYZug3X=@vMq;olqhY)1S;qy0d(AbF&+)v+)&G?=N8JbTTV zkwZo7ka^;=+aXTP0=A}BopDI@9!MI>ZZjz!=zYgp9- zDoBsx1SmhIrQ9e_L5y>0>+id<_aS~TCb-5NE`u{J7TSg8aDNL@f5e9)JgIH@b&{h+X7~DX%=TUIxU`R#mn;el?6dX8JJl{0T zigv=I_6K58m55-xcdAx{Zox&`mGv?eq2z@4m!LmA(O{{6w#6L8 zeH|@wgQ3u$5Es;+kGMGy!v&iD_oyNrOxNHjN|&W6Uo!WS@c|tj+$bOelD^6}U$YpB zAa<)*sfLPVEWyA3J1B}1ya}LtB`Vr`Wed#Ikp13@|Ite%5McL~0#(fxhGlU%UBM^s zs)>QEl?7Er-%Y;=NZ&v0fNJDr83KA0_xQD1Z_sE8d+4-wXEm}Pp~TY^e;Y?0WspW> zdUGqa60%hJLwy$3YVnzEm;kgKgN>y@x~Wb}P#xA?G|4n%9>4^fNealTR6OBhg4d#y z4z5gJmpiT9a@GG}Faw(r(=clswmw*wRwfnDWJ1pLm3!vxY z@v)|%X6yQ-VcL`S-$_|9Dge@s#poYV%S<*V2)4yb^4I?KzwsC3;uUjOWD#K&QFHbnNCKV0PqM!v!h9au^NSd8!npyM+t1!*1zBuyq%WZ zWRs=MG~Ww(z^2m5q~6GYG(f%W(x|%pPNjb;?L05Vq&hGG<%iuqSv5F_5~77$96z?C zPy=x4^Syo|i#BQ%)B7vtK$hkKL>aRJ+H-|j^dF?=Zork;UW{t)@oknS=qb4gt#Bg)e7AeGRB zXutFo596RrzNx6V;N412535%+xyX;z*pH8+D%wU!*c}VK3%gLHVSr+P^!i`bi$fC| zLeQw}2gAei2a~&zYQz|Qxls*n<-4x~Hq)^#F81$&qYs{q@%Q9E5urxN_^oVx6Dwll<%r9u2c-t<5mLU4Of%&QETHQ3StmKx~iFdALjZ_B*+P|KURSc#8 z$1V4;*eVv|0L1PTPiZR$K`HxgV0&2f6XJq9P)=CheqQX)Wu|PkQ-`2$7xXwvO%2xC8~>maq`O7rL}6vnb=sO!4v?zCe^o#@i+kmNT6u)uq}oABb1^R=N0S?RCpK^ zge+4&#yF2FH*$tu(^}+-hxqlWYM2itkgKpMjr@+u8U~_CmC|sw8;e52`y`Ad1|zcw zAER_n5dd3+gx=8Yw{A_Ol^N@DAZ;a<>;doI`Un5Qn`)(liEM0a5)_6Vi}v!Rk~6B= zMAN|6qL@V7%qOSdCk6imA1c740M<*f3Yk7WLfjfq&gO(!V9WETZ=j_#z=CC)c(IR% zOL)={{uQJcja{73a61XgdHAJ!yo=SKJuI6NY!0GA+EM8gnb(4GALgQtFI8jx>am0~ zYeMKLC`4I_K?J2F8Mv2>U#QBH7+;gybWxxWMjCHdN%02J9*LO-IV^TyL*=o^!>?e zYV}*bXomn&(2Lt;0*FeDN41+da7Z0U3H}A!-rc#_f7!rzeC)nHuN1)F|Ba&iLtbvO z#0eB~G8ITBLEL&8so`rd9l$E!NiMm6YM$Dg6&0-989;Ynf!y9C%gO9*2F+W<5<}g` zt}lq1bHFl;*iPc1Cwr@Nr2aGp9*A;>v0x;nz7di@>IvjsH|+v3Q(sZrg`gwfW0i#X z!9jMeF~u5AxG*ykme*8|6gZ>7 z?(j?tb8V(BX78VN%_+O#x_gmt!rV%`2fwVL?uGTr%}xg3iy3Hc4um51q~e}7D?vo- zqvRZwI`MY1I{ghYCeH(uU~#96H)Yce+{!IMldUN@mmwD0%I}LhafWA#e4dRSxT)f% z+nTIXAYviTa9oldCVY_?C7^`h-Mw8QE0ne1E$zH89X4v69G9xJ>{LX$=NW)LYQ&ar zUZVB`^tB1lIzU8YB)O$g;8I$#VGM2-XPxGMEZR4;ha;093VijJ)$l}xMc79WR^CSi zeV2BL(fQ1sfIjR*i8f=*4NEUVl13qjCQR`~0$U8(`4-W{>x@LWVG}Q1Mm}|=O>@U| z>WWmv3nYPl!uosc;On%x_;er!-I0WTdIonnt?v~po%#Dz7*fdAh$M2<7}L~0_=bt( z^&`r{0-eei3-HO2<%yta_4q1SzjLtVPYBKjf(CUOWU!~P-aKkP5YK;>T79qDW!>E{GHAxe+K4=AMUFTGM2q8%`tr5+L1?(n z(cMDDIBX;K_g&76c&ZLHs;#F%7(fjd-*LDM&_8N?$^OKeGZi9_uBtQt%;GCeMeFzp z^=?Sp4k^?g!PJ6_Myn9BuftqUbu_j-&h@;Pb+IrnUR4$%(euWlS{2U=uL?anH^?0o zmwV$EO_{D(PxX2&=nG-O-aiy)Qsn|{KW`~!*_MFZX#%oR<%F5c_F-{8ku;)@kva{| zLk#Vgkb9V0A|x*h7iyz7M1ubV&b;>72JXQpFt8mBEWhe4-iO#H(J1+I#Ix;>ehWv! zFcq7T$QYl!DDtmryJ0oVE}~$A^hmzQg6WR;M2i9sCg2}3 z`D@8^D&{L9d_aUtA!@XlkR<79q9A2lbE)}S)#L0T9Mer{R1l&z0yoeO4>G zoG$~0OP9?*r$>J)s||L&OF_KaGf@<1&nVJ(K(N@M*Y=w-+A<2c-$k}yLFOToBNL&n z&Q*`#{pLl#t(EHo{L(*p-hynlCQ<@K)DT9@@Z8J; za@4vN`kS}IB5gB-Wt-^HgT#rF7mwsXEyU6h#Iw>xrl9M}a36aobb&YdfrDZ-RVXtt zCY7bVIHg5`75Q&rB$!^DfXW+9`$JHttx@nW+52qESLNXhz8p`X$^4t>VH2&{i zza1_<#csU*8O;|XYT4YJGHM8SA;jJXegDmm?l53U1Bpjc5P40}g3xoR-4jpZ5EXT|FIFG&}@nb`Vw8cgs7y1E{we|ae zkh-T?_>3PhSrhLyx0%#5lC>%GjHMEw_JB2FLQ^M_9LJLP?wtLCB~21X`5|H1F~)MY z@EzM}!gesLVus^#VW32Ita#ejlwJ!M-c}5olyI|u8g~8<;j~W=qy1RyWM@Q!-2>+* za0;GLx1PVAAl|cqzuR#c?PU)Za@1LN>&4mch0uGatWvn4k{~UNa^fi}{^iGsY&X6f7s)bebwbQkM z<+h`KWq)rRc0S2C!WUW%kHF|80|DJnR*vokHJi6+x$-CUTRL(9$|4EQjM*}I4~r3f z?bgJVlLAG=fj5q#@VWVZ8Jo_gk}9ZKnn!X*yoK9-{?wP&(bzRRvq^Mvym z{`0Ua;q;55KyX;C9P;!ZXhS6`6y-7HUAWpEgg#hf)XXsaQ?y(lXVF)jQPB80+=6M4 zcqF5c{Xkh*S4JFx<|L**H)ex~sWf48YJEDcEW9C=CVuxTpVk!XT{d$mNM&XfPC5g+ z6wRa1=c2J8uG8w~#Lw!Sj(48vMys)Qs9~4>!8tsqEFsgVX0iz2>dN%DQ2ojJPrN@Hd-F}iP8~*5n!)-TVSL}_9h(8(&ZfL$E(i-$b=azH+XncsHZ$YyR!$MZ;up9WcyERY4r9_d0z$Po& zgD+S7I8=6t(lFLMJ+f=H%_ls~C;fF2X~uk5YpqCXLv#``yy_z$!k^P2UC}xJQ+r8e zS_bO;`g-i2mxS^aL94*80si+sI62L9BNNwl%Ic;`8k2qQnzFb_s9dlJLTqAZDPmv7 zX2bBAPi@0VJ7Buk;P&EuIipw(l>sa3%cxq7w#9$(RrKa|DjcO0Av24Z zl^xb2CZhxGiNg`9Uz5RW-Vm=ZmZo}m07~qq;Z&Mh`J(H{`SU|QGHva6E0PEnG!?F; zTdf$3UX=FkfUL|}6q;E=T)MbRPYn?t+9iCQZwRx)Nz5XdM&5jnYmG%Z(DWPZ^Hzu( zZ%c1_bjv9+7DOG3zbYQcdcBhF@07)wAx~^Gkd=zuGA)I4mnZK`t|;y{O8>bqn1yhJ z(D2Kl0%@hc3YntpzXsN!N**^>fOtdlqwQ)0VPlx25S~>@6#6_(OkAx}VWCJb7Q_ph z5H?ZS1Z+Q96%~%!Z*(ZNsq7$!B?G0bP*An+96!%KU@Ic5mO~R zJ-#eqym0ilY8lB$tuEIXR3XpP-`W@e=ybuRnVGO7Ovk79E_|q9^j0yCPrrKy%n% z`FjbhOy-QMJ--#1V$j!tvB&Fi2$?;828zd#RIkaUMGmv$Z2(F4>j$PV4an+C)N8z;fz<%RICT9KkL&Izq`z$t}Up{}t`1#`j z<+3O7DiN|)7IfXjB;^KOrcgGC{|;7NgbSnL2%seUM6V3cMZaDPBkV>OEc} zA<1f9+fXj}xMPc%2`pI+fc(z*0Vm-v!hbnBw_v^^0cxte}=+aqx&r<3ut|tv+=Q0n5A66!#3H zYRo6kblRCR>1u5YOa9HK${pkn2Oymuloxj$3&8KLLVGF^$gCiNW=m|J|0!U zw;x-hjeaYEfN=G!j+d`JejU=|2Hn92Y{7DIBIRUFl5C@*x_HD%!*=lPLfOswnY$*M z_l~-rx*Qy6WloGWg8vXJ^~Z5;6td9v4}yfE_r{glg+|Jb-9t&G-OgC&lx4&IriJ|_ z$$}|g)D;?WFck%_p=3Yn9e7Y^1rhlDWOP{he(5zafG=I*J` z*oS#2b`Ky4h+vi5fnjZt_~GIFx4C8L-_rW3rt#&M^Pj$tNzWZ;#cyWO30?8RNJuCd zF3+=?Gz@Q7SwU~MPFaO*N`%c>CK~H5H$(!Y<1J=J*&iM?Xo*Pp)Xo&HAXHkXbxpIJ zfQ`8w&YhaFqz{*d*kaBrZhQZ}63%)#CZ2pz7JzN3jDebV0OB1zIF&T{YiqlZuAMTk z>FzUYTzA_KOuU~SeMMt~qqq#(T+R-3c7qT(N0y4irRI!Sp%aKWe6iOa5f8wA-#d(Y z!!^a0`ucL1k|Rw^&aSX~z&qKT83jfdYxHUJ-dYThKIl?Bdw_QHWNmrs`1LT1GNrbl zaQZDiSuv`xMB9^j5X?7fpwhYI65)qOy2g{kb=chHI=ILPheF=;;3txN&_N}Fp|~+1 zF@3i8bt*?95Cc+9$gmFY=>n4b8Oyv_X7kJUA_~dp>haQO249mqiVE)wDkkZOyk2-? zYI8kIDQ6n5tDQ2ya$Rmys5ALy0Kwbe7`C{fbrPmmi7x-^jA9x+-u1DFJs*I27Y^!Y!U{BtH%ye=qkn1}^A!-+W)#aU|bf{cf;yX;nESdh+=~AO` z6a}F^C*BZJOcK{m(Z#IY1a4X8d{A?F%mmhIUyVcWD!vL-FVKv+Us0t7DJ9~8&}b}Z zhF$eC>*x+aj)h1qhi><`GXlyIW zXxEVVN;Q*nHn5A?i*<33ufRrdY7yY&i&^5BM!v)-v3+y9s)e4%T@Q#9_GyI|k6ylF z{BqVqA`_#G<*six0Lc;5D`iPBZcLMQyso{(nh{laoghCzS?aK+mTgJ*oT{H05x&RD zN0Yt>KsqHalUecG7*mo-s8t$L_%VqBM}p`{OWEy&nkT z^N7U=g1-1vr8>$iXR=CVlR8`}b~&zKJNNW4^evohA`v{0m{_%=ba#e-xT<~noae)4 zxS+2@Yy*>Mtecj?S-cuZ>AsvD9o&`%f#XxX^8^if*x-t3xjs9xE99ENUKU^bc#TC;jzCX?SbVOU zKUr4)4p!{~p}NZc>}Llhgy8&&Mo2ukwH-(_I8Ub6GK*pUv1VAGQubqR2wgSBhBa`b z8ZI@cz!a&IMKZ_O#m(iV;1Q~c%0?ffIGn8w zyGv%vw83#USExfdI#!o&Kv79K9(vEHFdSn+=iRAqsIN8L{)9ke?vL*Q>NvFYoZqK% zD!yvomd(htKtivT%fH{Yp0vu%iIf79k-Vn{CyV6mvZQP~UY%nsn9yWn-6{GeFgP6F#IfK0^1n>r-vNRq2)B%isuYIeji z;E^2e6?$6E%h1`#5b#SR68@_6tcZStwUzM>m-6`}FC)3S$(Y4vyxtg+S8Bz7UKy(p z5~<<(xebIyk+e;Oi>RT#cOm(!u_>!oS=qHxaVIqdj)M_i?V&-=jOk8PdZx<3DyLJ# zz$`qHaN{zt z*wmF{_~sVe$08%!FgoIA8sy6zRUI8m6}4WnVf`3f_y&4(VsU$Ed$9_miZIu2ZY@#e zWUGxVU$5va=e%Wmn|~sIq;(fnBo{kaDBf3fN)EqIvvPM>c8B}2VQKBw@uz2xA^3Kx zTt>M#L1#tauyZ4t-0r8)sUduh9Jl&6j7C8G-!sKqDvTz3riJ@>DIpu&q>Qd9I+ie` zW0KU9k3akh&UmV1#$Y|uNDfGm21URy8orhY^n-Mk$YHn@6M{LGfRLnff+L*}e^0r)88jz)nT1N~>)s~YFzG$d~%tx5Z}Ld(yOvT`SdpHb(| zMV?k(;}=?U<|TcYp>!N`5(MW$5~0om~3>*tZ( zwO0{J(8-THotx&o{_YRvg&yg?RZpvMCw^ez054UaS%VUY%RHnza(tm2LW+=e7ZK0^l6+~P^q)jh1riC4jX z0UCvU>B!_%2GT@HKizQ8@`Xf1Y~B$G3*O2?KBhHEDMfiIUXHNzGqigvkE}|ni5Dpz zb)3iS4J6_g&8x+oHtq-i{!CHBu}49=>UD7zO8+r7I%*P>ZQT_APIiML!v#@0_t}xF z0A+ipwqqDupAo?#9#Q;f>i~T~!T0jYhfiLOkt`Ak2amKxEdRu1aDhfNo%lse0lV*R z_J_7C?5=%|_CmUE9Nj#r$3Uh#C}7{GZ`M!YMu9h*-$~7P-Y=uVhZ8a;Uj;kB zaBq8o02XAP$JyZlJm6znsP+xanGuIv#pJ8_T4!2KBkR5e<-RJ#d5I5Uw0)PfEg3Tf zW~Jyt5gM?Dh3|d#^Em4qkt2t_r&#y3^wBwk{U1eN`LOOOJE5$&T%e;P)ix;`3AhoV z`Axrch=y3k$!+)7(t|uIG(lxPI)77+tjgVdr&<8VSPkDyRzE{5R;deH|_j7{hh{+rNWveCfY=rU39CmfPTx4hfrb# zMC$;3u&{b?xjPsh?4%taIU4d0ci~~*NKLI0+3nH#)6MzgAALj*kcr2_+by<_TGL}w zdQfFwQqO{j;dA(3flQ&DCHdf3VlF|?KGZeC;`kWBx!^~|$=z>tjB*dDBn}q@f+B1d zN$QQVqQHVBD9sVeK~56Bf{FAhhmgqd^t)3My#g(vp9AITWt_0zP*uDH=&`wr{M}0`p|Pozm-vBW z?0ErUm%d4AWV~4Uyvjq8CRLRIb0ocv*2jE1lGMT_ny5Ti=t#5H^&zv>eN?i_{LJuIY$OIbMUu zonCYZ+uJ2|cA82gi3e#YP##NVD^il{ORmqjTCeBE0)zN(zMuN|zOn%JiKh@N)R>eM zACY7Px!)MO#+1*P*x%hew|;PzNTe4gVO;AZr>VVtmg0qit?T*M-t%HdVwhDLR?iNm z1YjY>PJQ3x4zqI~Gxb`spK#!~ z4Hamz%d-|EGQ-JD=r<`LRd(*Ssl|u+?g!(ckZDBT>TYwLJL256vWtLP3Dth^q_sRe z86M5*V>X&T>SY}B*32)@h^{Np2DN8~Ioy=sTQ4g_G*X87j&mHKnUC{WPd9gD-qlE67%2I3aRlAH6Q%y3B;?WA_6&O9<8GWPek$ON#Z?L9*&* zeByNJQF+Pxt`_|;M>v9kDtm?FQjRBg=aE43k!I{xK_@=V(iqt9>t7;a?pNPX25Yjp zhF9Xgu{O8cA(z+HQl3&#oRt5Ipj^BhzTt(|p{c1nj>jGJoq>i2L4;!dyV1;*w{JDy zOXPo&3%T^VcHqn6UkX#C*>^Ox34DjWS)M9}of1Vx9AkAs7yR>v(J$^4m%zO}oG7#P z50CEJ5jiz+;g^~51<-!OJ%8(@aI3al2NSeE0_zEz`W9YL+Xjhpzez?r#c=tm*xL4= zQpda@5^Yp!&h)RYUR`p2v-$4K@oRXpLa4bXuMAu7PpROQvEHk#-I{oQlS?6?M-52Z z&!^_z47r@MsYN|Easga9djUAA{>DQF{N%jb%l$x$%@z&jrsgH{S;jUMOgX>L$ zS(@8w!=xW=ezlovl~gy>K^=+Ik1{=D@RR&2@7Yt*1%J~{eDP z&2-H4-47(RC25d{`z1T(!hzf7HY18v>Qhral&lzw83oBrCI+APL%og3|cHjJ~ zcpsX7an;`0B6y*XqcN9ki@C=d&yG4B#MQ(elw?t{7beEYn{B^&nmntkj(xHT?-9T%pTkCaWyy5?Tl zdvNT(LN&30GNoSJ{!KN(Os+yXV*IOJh5>=cv#}@T)>BBqFJsf}7WQ>l28)Vyx%RPy zpCyu^6J5h=h7hNRqgZ4>qntJ?zIe$vijnns5~a?XdiUr064{TBN1;{R^^!7MCnu?* z@}il8IbZtD#nx|hZ-oMj^jZSo*M&i4^O7ut;32 z`p32rLD@wqb|L7}*xXRBIQx&EUh3|^qHKrB6=w>DZFrf%LEXDlx3&ajbo~MoTAJH% zp*QBxkKT5^Gh+)wCQp9)+`f|!|FV)+js3&7{j~OciiJ9JkMh9$^7XXC z)zj=<7Sd2(pV=XuEaTn`TUD^ZlVW4`G?Ua!O5J)dyVFC~8{wzh+v_P5fa3d!q5#dU zSgl^t};!W0O|Mak8kJy ziWls>lh~&?wm7q?Bfv*J1v|6Y!vXCuDqmQHAk?AsZKb1QEGVg&OkqbQhtc$iy~MS{ zJSq>v+kWC-W&3J#6n>ZYmvVd$+9tQh%=qWK2W4UXIR5J}Szt4Xj(54hA&^BJ&v{XA zBY0B7$k65CGgh{JB&U-6$4l=-hGSJnCN-0h!$yLzE|JjK{? z?Yu>eDaNaj(EsG|+Z;Mz4|g5z3_h>LC$-zJoOcD1s(&y1C11=D<-}9s*ZV*aZ~cZn z8?W-{{Z$o_kUL84w|IoC?g381qgFSk6mHv`uIv(*-{~dM(Ucyl>rMSPp)A>h`~VSa%DO z00Sc5m>=qG2h`-!X3BVrxPNN8V;-U(iU<*U zRMXz3yR4!2qbt9!8^zw2;#ih(F8bU)F)N6HHTQ#jx)w&R0|4*=B8!1&2> ziqf4#{!AOW?EMT63|9NRP+lA9?=nnA=uq`7T}%}Syn4Ph**;CPC?INpZ7Q{37r*1H zOQXuJCb~MxCU*!66P4Z^*xSGBAGmv7K#t?8e^dkj%pZ^aT&;G=Ggt55q|Lpd z^SNAem7zM@_WVuGEexF7KL*D<2y#6(*OTPo%RpU8FzbYGijT08MhU%*IwRC2ecw-} zw|D=5BiFVo^v6$q8#W03`>Gc6I|S$Kv+^#-lx_;vf%PFCBi zDSfF8x^DR{xh_8!IC&f5-bMor;M#$L+E5nv%4GiMi3O*dNq5vH|K4{I7h7A-->P1n zGr`+|LD*!$Zw4==Ui_9~bBuRKYm>G4##P*Vg})gUcKPBSTsrg9!@a?~TgcUi@9OhO z#D#CLKX3e#2=2OL!XeHXQQ{71at*l4x5?n&O|4<`7KHbq;2qLyV>U`c#kf?tC4Q#%$ zdBg=^9&w}6ZzRf*lXcdMUdz2{Z;{q&Jdm1~_jM=F1A=1$u7`k(Y|4)Skk^Af{WiF% zh=d?_`v=*P(9~~?$pqy?6^0uR9H*o(&(pH5hf8MAMX@GU2negmqg%malri~3%@WY9 zUG5PM|4o{T`7ThhYj>ye@9NF-C8GJY^v+W86FLYGi4w|t{M}@;h5SlJ*xxMYfYXXhX7zT&dm%MTBe#2S!{5cN|e!Z-aN zW+(1?&58h$7s@hQ3&F@)7a|GYK2zPPN@dHlq)A+3NwxJDETJdOf*vltyH_D97O+P9 zo<6i+k?M~rnTafOstnS$k$>&43*>>d1uMxHzv0gwwc%mD_PmQ>w;&X3Nq(3>w71wD zHG*0fKNV8DgS=WwhXJ^3N!Y>Bgl!+I=(LerpZXVk76aChu|gyzi5tI>jWcc6#yPgi=pzc!@F`mRbrhH6xHR+<%7 zf&i_}LA165dRC4f9R+FZ&jf9Fn-20RLRL6V`M*AP$0E6+VEnxzt2^;HT+Wa|-u;^) zcHC?je^gBcyb6R}&%+|KBA+*JfQB%N2aW2ED?V53;>Wz;G*&7A_yT<#&Wzl3_Iu4O zJw*1G_MDb3Ob#BYd~JIU0Gs)O@IOwvCe(06vvB8Qga?6y4e1wfS`ZB5s&A}d+tV|w zYnr+OaYpZ+l@Zx5%A=0g*A3fMGp)a^>+PR#Eg3p$$pc~TgKFN#D z*Z=;0=E4O4yCq=l1{5_b`iC8eyp_^qhB}%Sq}l;dJpfLf4SOQStA?M;P~a1z1-4$& zyw;6J{v8<@093t4y$R!_TA!w*3h06_=Pan+oxfeBm_Gb*mrR1NymO)7#H~;_(Y3bB;5qPm`V4VN~007X-_22yk2fT>-{|X>^`wv@4AJZam z9}%Ip?!Ldb@gk(^0-1db83yz;n}ux2xdVpSGSIpNo7(p&C*mX$Ko10_>wFl2A_-6d z7LVi>_t-FGV6XAX=>hZ-{M4_X#w`B_gCW!*|(SO(> z!N-=JxfDKxm+_Iw59KgQ%;Ph!x1XIVgh$q?0T4`ZCVQ=5tOtwrLEkUcF!*JPmRT6% zgzw8l2I2g$))`oSf)h{M$A8E#0zI0BNC@9+^xLXQy-0Oj1FG$-`;WIP0;b^!n&FSC zx?a_{cOV=u)yMk=;^Q5KZyxF0xt@% zPou%3bPpo_jQ#Q;zX~pUM`H5daHRqa^sZ zws&MdIfpu$Xc;^5u_Vp=LA+J6b}b@v$BC? PnSpQ}kp2slXJ7yTqc8-m literal 0 HcmV?d00001 diff --git a/test/models/Collada/duck_nomanifest.zae b/test/models/Collada/duck_nomanifest.zae new file mode 100644 index 0000000000000000000000000000000000000000..d5559ccb2f93d22385783c35e9d47ca8dbf11e1a GIT binary patch literal 132634 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

Vi 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 =Geb5c%^8 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

>38$c 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

cXK<%%4toD?kC_HmG#_L9 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 literal 0 HcmV?d00001 From 8636d5607c0edf4f528992dff290dda89ea536d0 Mon Sep 17 00:00:00 2001 From: RichardTea <31507749+RichardTea@users.noreply.github.com> Date: Fri, 12 Jul 2019 11:35:43 +0100 Subject: [PATCH 03/19] Add ZipArchiveIOSystem to CMake --- code/CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/code/CMakeLists.txt b/code/CMakeLists.txt index 9c484910c..8d3f6ee28 100644 --- a/code/CMakeLists.txt +++ b/code/CMakeLists.txt @@ -104,6 +104,7 @@ SET( PUBLIC_HEADERS ${HEADER_PATH}/Exporter.hpp ${HEADER_PATH}/DefaultIOStream.h ${HEADER_PATH}/DefaultIOSystem.h + ${HEADER_PATH}/ZipArchiveIOSystem.h ${HEADER_PATH}/SceneCombiner.h ${HEADER_PATH}/fast_atof.h ${HEADER_PATH}/qnan.h @@ -172,6 +173,7 @@ SET( Common_SRCS Common/DefaultProgressHandler.h Common/DefaultIOStream.cpp Common/DefaultIOSystem.cpp + Common/ZipArchiveIOSystem.cpp Common/PolyTools.h Common/Importer.cpp Common/IFF.h From 2a72c9b616a2018cb9683094120e68016cf2d079 Mon Sep 17 00:00:00 2001 From: RichardTea <31507749+RichardTea@users.noreply.github.com> Date: Fri, 12 Jul 2019 14:46:48 +0100 Subject: [PATCH 04/19] Read embedded textures from Collada ZAE --- code/Collada/ColladaHelper.h | 8 ++------ code/Collada/ColladaLoader.cpp | 36 ++++++++++++++++++---------------- code/Collada/ColladaLoader.h | 6 +++--- code/Collada/ColladaParser.cpp | 22 ++++++++++++++++++++- code/Collada/ColladaParser.h | 3 +++ 5 files changed, 48 insertions(+), 27 deletions(-) diff --git a/code/Collada/ColladaHelper.h b/code/Collada/ColladaHelper.h index ffab6226d..66cff2d20 100644 --- a/code/Collada/ColladaHelper.h +++ b/code/Collada/ColladaHelper.h @@ -580,15 +580,11 @@ struct Image { std::string mFileName; - /** If image file name is zero, embedded image data - */ + /** Embedded image data */ std::vector mImageData; - /** If image file name is zero, file format of - * embedded image data. - */ + /** File format hint ofembedded image data */ std::string mEmbeddedFormat; - }; /** An animation channel. */ diff --git a/code/Collada/ColladaLoader.cpp b/code/Collada/ColladaLoader.cpp index 40b2b0811..bcf6b1c60 100644 --- a/code/Collada/ColladaLoader.cpp +++ b/code/Collada/ColladaLoader.cpp @@ -235,6 +235,9 @@ void ColladaLoader::InternReadFile(const std::string& pFile, aiScene* pScene, IO // store all materials StoreSceneMaterials(pScene); + // store all textures + StoreSceneTextures(pScene); + // store all lights StoreSceneLights(pScene); @@ -927,8 +930,7 @@ void ColladaLoader::StoreSceneLights(aiScene* pScene) void ColladaLoader::StoreSceneTextures(aiScene* pScene) { pScene->mNumTextures = static_cast(mTextures.size()); - if (mTextures.size() > 0) - { + if (mTextures.size() > 0) { pScene->mTextures = new aiTexture*[mTextures.size()]; std::copy(mTextures.begin(), mTextures.end(), pScene->mTextures); mTextures.clear(); @@ -1728,7 +1730,7 @@ void ColladaLoader::BuildMaterials(ColladaParser& pParser, aiScene* /*pScene*/) mat->AddProperty(&colSpecular, 1, AI_MATKEY_COLOR_SPECULAR); const ai_real specExp = 5.0; mat->AddProperty(&specExp, 1, AI_MATKEY_SHININESS); - } +} #endif } @@ -1766,14 +1768,19 @@ aiString ColladaLoader::FindFilenameForEffectTexture(const ColladaParser& pParse } // if this is an embedded texture image setup an aiTexture for it - if (imIt->second.mFileName.empty()) + if (!imIt->second.mImageData.empty()) { - if (imIt->second.mImageData.empty()) { - throw DeadlyImportError("Collada: Invalid texture, no data or file reference given"); - } - aiTexture* tex = new aiTexture(); + // Store embedded texture name reference + tex->mFilename.Set(imIt->second.mFileName.c_str()); + result.Set(imIt->second.mFileName); + + // TODO: check the possibility of using the flag "AI_CONFIG_IMPORT_FBX_EMBEDDED_TEXTURES_LEGACY_NAMING" +// result.data[0] = '*'; +// result.length = 1 + ASSIMP_itoa10(result.data + 1, static_cast(MAXLEN - 1), static_cast(mTextures.size())); + + // setup format hint if (imIt->second.mEmbeddedFormat.length() > 3) { ASSIMP_LOG_WARN("Collada: texture format hint is too long, truncating to 3 characters"); @@ -1786,20 +1793,15 @@ aiString ColladaLoader::FindFilenameForEffectTexture(const ColladaParser& pParse tex->pcData = (aiTexel*)new char[tex->mWidth]; memcpy(tex->pcData, &imIt->second.mImageData[0], tex->mWidth); - // TODO: check the possibility of using the flag "AI_CONFIG_IMPORT_FBX_EMBEDDED_TEXTURES_LEGACY_NAMING" - // In FBX files textures are now stored internally by Assimp with their filename included - // Now Assimp can lookup through the loaded textures after all data is processed - // We need to load all textures before referencing them, as FBX file format order may reference a texture before loading it - // This may occur on this case too, it has to be studied - // setup texture reference string - result.data[0] = '*'; - result.length = 1 + ASSIMP_itoa10(result.data + 1, static_cast(MAXLEN - 1), static_cast(mTextures.size())); - // and add this texture to the list mTextures.push_back(tex); } else { + if (imIt->second.mFileName.empty()) { + throw DeadlyImportError("Collada: Invalid texture, no data or file reference given"); + } + result.Set(imIt->second.mFileName); ConvertPath(result); } diff --git a/code/Collada/ColladaLoader.h b/code/Collada/ColladaLoader.h index ad606ea74..92f390f17 100644 --- a/code/Collada/ColladaLoader.h +++ b/code/Collada/ColladaLoader.h @@ -100,14 +100,14 @@ protected: /** Return importer meta information. * See #BaseImporter::GetInfo for the details */ - const aiImporterDesc* GetInfo () const; + const aiImporterDesc* GetInfo () const override; - void SetupProperties(const Importer* pImp); + void SetupProperties(const Importer* pImp) override; /** Imports the given file into the given scene structure. * See BaseImporter::InternReadFile() for details */ - void InternReadFile( const std::string& pFile, aiScene* pScene, IOSystem* pIOHandler); + void InternReadFile( const std::string& pFile, aiScene* pScene, IOSystem* pIOHandler) override; /** Recursively constructs a scene node for the given parser node and returns it. */ aiNode* BuildHierarchy( const ColladaParser& pParser, const Collada::Node* pNode); diff --git a/code/Collada/ColladaParser.cpp b/code/Collada/ColladaParser.cpp index 560c0cc9d..20a881860 100644 --- a/code/Collada/ColladaParser.cpp +++ b/code/Collada/ColladaParser.cpp @@ -132,7 +132,7 @@ ColladaParser::ColladaParser(IOSystem* pIOHandler, const std::string& pFile) // read embedded textures if (zip_archive && zip_archive->isOpen()) { - // TODO + ReadEmbeddedTextures(*zip_archive); } } @@ -3056,6 +3056,26 @@ void ColladaParser::ReadMaterialVertexInputBinding(Collada::SemanticMappingTable } } +void Assimp::ColladaParser::ReadEmbeddedTextures(ZipArchiveIOSystem& zip_archive) +{ + // Attempt to load any undefined Collada::Image in ImageLibrary + for (ImageLibrary::iterator it = mImageLibrary.begin(); it != mImageLibrary.end(); ++it) { + Collada::Image &image = (*it).second; + + if (image.mImageData.empty()) { + std::unique_ptr image_file(zip_archive.Open(image.mFileName.c_str())); + if (image_file) { + image.mImageData.resize(image_file->FileSize()); + image_file->Read(image.mImageData.data(), image_file->FileSize(), 1); + image.mEmbeddedFormat = BaseImporter::GetExtension(image.mFileName); + if (image.mEmbeddedFormat == "jpeg") { + image.mEmbeddedFormat = "jpg"; + } + } + } + } +} + // ------------------------------------------------------------------------------------------------ // Reads a mesh reference in a node and adds it to the node's mesh list void ColladaParser::ReadNodeGeometry(Node* pNode) diff --git a/code/Collada/ColladaParser.h b/code/Collada/ColladaParser.h index 7c77013f6..a2c9a4ff2 100644 --- a/code/Collada/ColladaParser.h +++ b/code/Collada/ColladaParser.h @@ -239,6 +239,9 @@ namespace Assimp // Processes bind_vertex_input and bind elements void ReadMaterialVertexInputBinding( Collada::SemanticMappingTable& tbl); + /** Reads embedded textures from a ZAE archive*/ + void ReadEmbeddedTextures(ZipArchiveIOSystem &zip_archive); + protected: /** Aborts the file reading with an exception */ AI_WONT_RETURN void ThrowException( const std::string& pError) const AI_WONT_RETURN_SUFFIX; From 0c718a9c95d7d48d9dba832d35772efabefa2f88 Mon Sep 17 00:00:00 2001 From: RichardTea <31507749+RichardTea@users.noreply.github.com> Date: Fri, 12 Jul 2019 14:56:29 +0100 Subject: [PATCH 05/19] Add Collada ZAE unit test --- test/unit/utColladaImportExport.cpp | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/test/unit/utColladaImportExport.cpp b/test/unit/utColladaImportExport.cpp index 78a74d973..1a2a6bc9d 100644 --- a/test/unit/utColladaImportExport.cpp +++ b/test/unit/utColladaImportExport.cpp @@ -57,6 +57,19 @@ public: } }; -TEST_F( utColladaImportExport, importBlenFromFileTest ) { - EXPECT_TRUE( importerTest() ); +TEST_F(utColladaImportExport, importBlenFromFileTest) { + EXPECT_TRUE(importerTest()); +} + +class utColladaZaeImportExport : public AbstractImportExportBase { +public: + virtual bool importerTest() { + Assimp::Importer importer; + const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MODELS_DIR "/Collada/duck.zae", aiProcess_ValidateDataStructure); + return nullptr != scene; + } +}; + +TEST_F(utColladaZaeImportExport, importBlenFromFileTest) { + EXPECT_TRUE(importerTest()); } From 678dd32da3c772831b210a348eda7069c84b31ad Mon Sep 17 00:00:00 2001 From: RichardTea <31507749+RichardTea@users.noreply.github.com> Date: Fri, 12 Jul 2019 15:37:10 +0100 Subject: [PATCH 06/19] Add missing header --- code/Common/ZipArchiveIOSystem.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/code/Common/ZipArchiveIOSystem.cpp b/code/Common/ZipArchiveIOSystem.cpp index 5a42a0dcb..cff1109c2 100644 --- a/code/Common/ZipArchiveIOSystem.cpp +++ b/code/Common/ZipArchiveIOSystem.cpp @@ -46,6 +46,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include +#include #ifdef ASSIMP_USE_HUNTER # include From 07d3a7d5b075ad091403bd3330b0af9fc1457658 Mon Sep 17 00:00:00 2001 From: RichardTea <31507749+RichardTea@users.noreply.github.com> Date: Fri, 12 Jul 2019 16:02:51 +0100 Subject: [PATCH 07/19] Fix some warnings Hopefully the sorts out the MSVC2013 compiler --- code/Common/ZipArchiveIOSystem.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/code/Common/ZipArchiveIOSystem.cpp b/code/Common/ZipArchiveIOSystem.cpp index cff1109c2..8079db0b8 100644 --- a/code/Common/ZipArchiveIOSystem.cpp +++ b/code/Common/ZipArchiveIOSystem.cpp @@ -199,20 +199,23 @@ namespace Assimp { explicit ZipFileInfo(unzFile zip_handle, size_t size); // Allocate and Extract data from the ZIP - ZipFile * Extract(unzFile zip_handle, const std::string & file) const; + ZipFile * Extract(unzFile zip_handle) const; private: size_t m_Size = 0; - unz_file_pos_s m_ZipFilePos = { 0,0 }; + unz_file_pos_s m_ZipFilePos; }; ZipFileInfo::ZipFileInfo(unzFile zip_handle, size_t size) : m_Size(size) { ai_assert(m_Size != 0); + // Workaround for MSVC 2013 - C2797 + m_ZipFilePos.num_of_file = 0; + m_ZipFilePos.pos_in_zip_directory = 0; unzGetFilePos(zip_handle, &(m_ZipFilePos)); } - ZipFile * ZipFileInfo::Extract(unzFile zip_handle, const std::string & file) const { + ZipFile * ZipFileInfo::Extract(unzFile zip_handle) const { // Find in the ZIP. This cannot fail unz_file_pos_s *filepos = const_cast(&(m_ZipFilePos)); if (unzGoToFilePos(zip_handle, filepos) != UNZ_OK) @@ -385,8 +388,6 @@ namespace Assimp { MapArchive(); ZipFileMap::const_iterator it = m_ArchiveMap.find(filename); - bool exist(false); - return (it != m_ArchiveMap.end()); } @@ -401,7 +402,7 @@ namespace Assimp { return nullptr; const ZipFileInfo &zip_file = (*zip_it).second; - return zip_file.Extract(m_ZipFileHandle, filename); + return zip_file.Extract(m_ZipFileHandle); } inline void ReplaceAll(std::string& data, const std::string& before, const std::string& after) { From c6620478ecda02de87d16ad4084ead03b0387782 Mon Sep 17 00:00:00 2001 From: RichardTea <31507749+RichardTea@users.noreply.github.com> Date: Wed, 24 Jul 2019 16:12:06 +0100 Subject: [PATCH 08/19] Fix GCC warnings reported by Travis --- code/Common/ZipArchiveIOSystem.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/code/Common/ZipArchiveIOSystem.cpp b/code/Common/ZipArchiveIOSystem.cpp index 8079db0b8..463c2071b 100644 --- a/code/Common/ZipArchiveIOSystem.cpp +++ b/code/Common/ZipArchiveIOSystem.cpp @@ -226,7 +226,7 @@ namespace Assimp { ZipFile *zip_file = new ZipFile(m_Size); - if (unzReadCurrentFile(zip_handle, zip_file->m_Buffer.get(), static_cast(m_Size)) != m_Size) + if (unzReadCurrentFile(zip_handle, zip_file->m_Buffer.get(), static_cast(m_Size)) != static_cast(m_Size)) { // Failed, release the memory delete zip_file; @@ -281,6 +281,7 @@ namespace Assimp { m_SeekPtr = m_Size - pOffset; return aiReturn_SUCCESS; } + default:; } return aiReturn_FAILURE; @@ -355,7 +356,7 @@ namespace Assimp { if (fileInfo.uncompressed_size != 0) { std::string filename_string(filename, fileInfo.size_filename); SimplifyFilename(filename_string); - std::pair result = m_ArchiveMap.insert(std::make_pair(filename_string, ZipFileInfo(m_ZipFileHandle, fileInfo.uncompressed_size))); + m_ArchiveMap.emplace(filename_string, ZipFileInfo(m_ZipFileHandle, fileInfo.uncompressed_size)); } } } while (unzGoToNextFile(m_ZipFileHandle) != UNZ_END_OF_LIST_OF_FILE); From 9e04df810e75630bb43de6868111e547b1536d6a Mon Sep 17 00:00:00 2001 From: RichardTea <31507749+RichardTea@users.noreply.github.com> Date: Thu, 25 Jul 2019 09:53:18 +0100 Subject: [PATCH 09/19] ZipArchiveIOSystem should be sorted, fix ZipFile::Read() Q3BSP relies on the sort order Read() should return number of elements read, not count of bytes Read() should clip to the file size and return elements actually read, instead of aborting if try to read too much --- code/Common/ZipArchiveIOSystem.cpp | 33 ++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/code/Common/ZipArchiveIOSystem.cpp b/code/Common/ZipArchiveIOSystem.cpp index 463c2071b..43c66a921 100644 --- a/code/Common/ZipArchiveIOSystem.cpp +++ b/code/Common/ZipArchiveIOSystem.cpp @@ -45,7 +45,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include -#include +#include #include #ifdef ASSIMP_USE_HUNTER @@ -55,7 +55,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #endif namespace Assimp { - // ---------------------------------------------------------------- // Wraps an existing Assimp::IOSystem for unzip class IOSystem2Unzip { @@ -247,14 +246,25 @@ namespace Assimp { } size_t ZipFile::Read(void* pvBuffer, size_t pSize, size_t pCount) { - const size_t size = pSize * pCount; - ai_assert((size + m_SeekPtr) <= m_Size); + // Should be impossible + ai_assert(m_Buffer != nullptr); + ai_assert(NULL != pvBuffer && 0 != pSize && 0 != pCount); - std::memcpy(pvBuffer, m_Buffer.get() + m_SeekPtr, size); + // Clip down to file size + size_t byteSize = pSize * pCount; + if ((byteSize + m_SeekPtr) > m_Size) + { + pCount = (m_Size - m_SeekPtr) / pSize; + byteSize = pSize * pCount; + if (byteSize == 0) + return 0; + } - m_SeekPtr += size; + std::memcpy(pvBuffer, m_Buffer.get() + m_SeekPtr, byteSize); - return size; + m_SeekPtr += byteSize; + + return pCount; } size_t ZipFile::FileSize() const { @@ -312,9 +322,10 @@ namespace Assimp { void MapArchive(); private: + typedef std::map ZipFileInfoMap; + unzFile m_ZipFileHandle = nullptr; - typedef std::unordered_map ZipFileMap; - ZipFileMap m_ArchiveMap; + ZipFileInfoMap m_ArchiveMap; }; ZipArchiveIOSystem::Implement::Implement(IOSystem* pIOHandler, const char* pFilename, const char* pMode) { @@ -388,7 +399,7 @@ namespace Assimp { bool ZipArchiveIOSystem::Implement::Exists(std::string& filename) { MapArchive(); - ZipFileMap::const_iterator it = m_ArchiveMap.find(filename); + ZipFileInfoMap::const_iterator it = m_ArchiveMap.find(filename); return (it != m_ArchiveMap.end()); } @@ -398,7 +409,7 @@ namespace Assimp { SimplifyFilename(filename); // Find in the map - ZipFileMap::const_iterator zip_it = m_ArchiveMap.find(filename); + ZipFileInfoMap::const_iterator zip_it = m_ArchiveMap.find(filename); if (zip_it == m_ArchiveMap.cend()) return nullptr; From 5c6ea5d7ba914dc52e07df28fb4c862bcddac28e Mon Sep 17 00:00:00 2001 From: RichardTea <31507749+RichardTea@users.noreply.github.com> Date: Thu, 25 Jul 2019 09:54:39 +0100 Subject: [PATCH 10/19] Fix memory leak in Collada ZAE and 3MF ZipFile reading Must close the file! --- code/3MF/D3MFOpcPackage.cpp | 6 +++--- code/Collada/ColladaParser.cpp | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/code/3MF/D3MFOpcPackage.cpp b/code/3MF/D3MFOpcPackage.cpp index 739e0cfcc..873ba8ee8 100644 --- a/code/3MF/D3MFOpcPackage.cpp +++ b/code/3MF/D3MFOpcPackage.cpp @@ -149,14 +149,14 @@ D3MFOpcPackage::D3MFOpcPackage(IOSystem* pIOHandler, const std::string& rFile) ASSIMP_LOG_DEBUG(rootFile); + mZipArchive->Close(fileStream); + mRootStream = mZipArchive->Open(rootFile.c_str()); ai_assert( mRootStream != nullptr ); if ( nullptr == mRootStream ) { throw DeadlyExportError( "Cannot open root-file in archive : " + rootFile ); } - mZipArchive->Close( fileStream ); - } else if( file == D3MF::XmlTag::CONTENT_TYPES_ARCHIVE) { ASSIMP_LOG_WARN_F("Ignored file of unsupported type CONTENT_TYPES_ARCHIVES",file); } else { @@ -167,7 +167,7 @@ D3MFOpcPackage::D3MFOpcPackage(IOSystem* pIOHandler, const std::string& rFile) } D3MFOpcPackage::~D3MFOpcPackage() { - // empty + mZipArchive->Close(mRootStream); } IOStream* D3MFOpcPackage::RootStream() const { diff --git a/code/Collada/ColladaParser.cpp b/code/Collada/ColladaParser.cpp index 20a881860..646ec473d 100644 --- a/code/Collada/ColladaParser.cpp +++ b/code/Collada/ColladaParser.cpp @@ -165,7 +165,7 @@ std::string ColladaParser::ReadZaeManifest(ZipArchiveIOSystem &zip_archive) { } std::unique_ptr mIOWrapper(new CIrrXML_IOStreamReader(manifestfile.get())); - irr::io::IrrXMLReader* manifest_reader = irr::io::createIrrXMLReader(mIOWrapper.get()); + std::unique_ptr manifest_reader(irr::io::createIrrXMLReader(mIOWrapper.get())); while (manifest_reader->read()) { From 3016f3a7256b20fa714a11fe6228d292e7add828 Mon Sep 17 00:00:00 2001 From: RichardTea <31507749+RichardTea@users.noreply.github.com> Date: Thu, 25 Jul 2019 10:13:53 +0100 Subject: [PATCH 11/19] Convert Q3BSP Importer to use ZipArchiveIOSystem Removes duplication, handles large files better --- code/CMakeLists.txt | 2 - code/Q3BSP/Q3BSPFileImporter.cpp | 16 +- code/Q3BSP/Q3BSPFileImporter.h | 12 +- code/Q3BSP/Q3BSPFileParser.cpp | 5 +- code/Q3BSP/Q3BSPFileParser.h | 13 +- code/Q3BSP/Q3BSPZipArchive.cpp | 325 ------------------------------- code/Q3BSP/Q3BSPZipArchive.h | 135 ------------- 7 files changed, 23 insertions(+), 485 deletions(-) delete mode 100644 code/Q3BSP/Q3BSPZipArchive.cpp delete mode 100644 code/Q3BSP/Q3BSPZipArchive.h diff --git a/code/CMakeLists.txt b/code/CMakeLists.txt index 5c1197364..110b211f8 100644 --- a/code/CMakeLists.txt +++ b/code/CMakeLists.txt @@ -690,8 +690,6 @@ ADD_ASSIMP_IMPORTER( Q3BSP Q3BSP/Q3BSPFileParser.cpp Q3BSP/Q3BSPFileImporter.h Q3BSP/Q3BSPFileImporter.cpp - Q3BSP/Q3BSPZipArchive.h - Q3BSP/Q3BSPZipArchive.cpp ) ADD_ASSIMP_IMPORTER( RAW diff --git a/code/Q3BSP/Q3BSPFileImporter.cpp b/code/Q3BSP/Q3BSPFileImporter.cpp index 6a3b95bb8..4add00a07 100644 --- a/code/Q3BSP/Q3BSPFileImporter.cpp +++ b/code/Q3BSP/Q3BSPFileImporter.cpp @@ -43,7 +43,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #ifndef ASSIMP_BUILD_NO_Q3BSP_IMPORTER #include "Q3BSPFileImporter.h" -#include "Q3BSPZipArchive.h" #include "Q3BSPFileParser.h" #include "Q3BSPFileData.h" @@ -60,6 +59,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include +#include #include #include #include @@ -181,7 +181,7 @@ const aiImporterDesc* Q3BSPFileImporter::GetInfo () const { // ------------------------------------------------------------------------------------------------ // Import method. void Q3BSPFileImporter::InternReadFile(const std::string &rFile, aiScene* scene, IOSystem* ioHandler) { - Q3BSPZipArchive Archive( ioHandler, rFile ); + ZipArchiveIOSystem Archive( ioHandler, rFile ); if ( !Archive.isOpen() ) { throw DeadlyImportError( "Failed to open file " + rFile + "." ); } @@ -223,10 +223,10 @@ void Q3BSPFileImporter::separateMapName( const std::string &importName, std::str // ------------------------------------------------------------------------------------------------ // Returns the first map in the map archive. -bool Q3BSPFileImporter::findFirstMapInArchive( Q3BSPZipArchive &bspArchive, std::string &mapName ) { +bool Q3BSPFileImporter::findFirstMapInArchive(ZipArchiveIOSystem &bspArchive, std::string &mapName ) { mapName = ""; std::vector fileList; - bspArchive.getFileList( fileList ); + bspArchive.getFileListExtension( fileList, "bsp" ); if (fileList.empty()) { return false; } @@ -249,7 +249,7 @@ bool Q3BSPFileImporter::findFirstMapInArchive( Q3BSPZipArchive &bspArchive, std: // ------------------------------------------------------------------------------------------------ // Creates the assimp specific data. void Q3BSPFileImporter::CreateDataFromImport( const Q3BSP::Q3BSPModel *pModel, aiScene* pScene, - Q3BSPZipArchive *pArchive ) { + ZipArchiveIOSystem *pArchive ) { if (nullptr == pModel || nullptr == pScene) { return; } @@ -418,7 +418,7 @@ void Q3BSPFileImporter::createTriangleTopology( const Q3BSP::Q3BSPModel *pModel, // ------------------------------------------------------------------------------------------------ // Creates all referenced materials. void Q3BSPFileImporter::createMaterials( const Q3BSP::Q3BSPModel *pModel, aiScene* pScene, - Q3BSPZipArchive *pArchive ) { + ZipArchiveIOSystem *pArchive ) { if ( m_MaterialLookupMap.empty() ) { return; } @@ -564,7 +564,7 @@ aiFace *Q3BSPFileImporter::getNextFace( aiMesh *mesh, unsigned int &faceIdx ) { // ------------------------------------------------------------------------------------------------ // Imports a texture file. bool Q3BSPFileImporter::importTextureFromArchive( const Q3BSP::Q3BSPModel *model, - Q3BSP::Q3BSPZipArchive *archive, aiScene*, + ZipArchiveIOSystem *archive, aiScene*, aiMaterial *pMatHelper, int textureId ) { if (nullptr == archive || nullptr == pMatHelper ) { return false; @@ -669,7 +669,7 @@ bool Q3BSPFileImporter::importLightmap( const Q3BSP::Q3BSPModel *pModel, aiScene // ------------------------------------------------------------------------------------------------ // Will search for a supported extension. -bool Q3BSPFileImporter::expandFile( Q3BSP::Q3BSPZipArchive *pArchive, const std::string &rFilename, +bool Q3BSPFileImporter::expandFile(ZipArchiveIOSystem *pArchive, const std::string &rFilename, const std::vector &rExtList, std::string &rFile, std::string &rExt ) { diff --git a/code/Q3BSP/Q3BSPFileImporter.h b/code/Q3BSP/Q3BSPFileImporter.h index 5af9fb7bc..ee21fa48e 100644 --- a/code/Q3BSP/Q3BSPFileImporter.h +++ b/code/Q3BSP/Q3BSPFileImporter.h @@ -54,9 +54,9 @@ struct aiMaterial; struct aiTexture; namespace Assimp { + class ZipArchiveIOSystem; namespace Q3BSP { - class Q3BSPZipArchive; struct Q3BSPModel; struct sQ3BSPFace; } @@ -85,24 +85,24 @@ protected: const aiImporterDesc* GetInfo () const; void InternReadFile(const std::string& pFile, aiScene* pScene, IOSystem* pIOHandler); void separateMapName( const std::string &rImportName, std::string &rArchiveName, std::string &rMapName ); - bool findFirstMapInArchive( Q3BSP::Q3BSPZipArchive &rArchive, std::string &rMapName ); - void CreateDataFromImport( const Q3BSP::Q3BSPModel *pModel, aiScene* pScene, Q3BSP::Q3BSPZipArchive *pArchive ); + bool findFirstMapInArchive(ZipArchiveIOSystem &rArchive, std::string &rMapName ); + void CreateDataFromImport( const Q3BSP::Q3BSPModel *pModel, aiScene* pScene, ZipArchiveIOSystem *pArchive ); void CreateNodes( const Q3BSP::Q3BSPModel *pModel, aiScene* pScene, aiNode *pParent ); aiNode *CreateTopology( const Q3BSP::Q3BSPModel *pModel, unsigned int materialIdx, std::vector &rArray, aiMesh **pMesh ); void createTriangleTopology( const Q3BSP::Q3BSPModel *pModel, Q3BSP::sQ3BSPFace *pQ3BSPFace, aiMesh* pMesh, unsigned int &rFaceIdx, unsigned int &rVertIdx ); - void createMaterials( const Q3BSP::Q3BSPModel *pModel, aiScene* pScene, Q3BSP::Q3BSPZipArchive *pArchive ); + void createMaterials( const Q3BSP::Q3BSPModel *pModel, aiScene* pScene, ZipArchiveIOSystem *pArchive ); size_t countData( const std::vector &rArray ) const; size_t countFaces( const std::vector &rArray ) const; size_t countTriangles( const std::vector &rArray ) const; void createMaterialMap( const Q3BSP::Q3BSPModel *pModel); aiFace *getNextFace( aiMesh *pMesh, unsigned int &rFaceIdx ); - bool importTextureFromArchive( const Q3BSP::Q3BSPModel *pModel, Q3BSP::Q3BSPZipArchive *pArchive, aiScene* pScene, + bool importTextureFromArchive( const Q3BSP::Q3BSPModel *pModel, ZipArchiveIOSystem *pArchive, aiScene* pScene, aiMaterial *pMatHelper, int textureId ); bool importLightmap( const Q3BSP::Q3BSPModel *pModel, aiScene* pScene, aiMaterial *pMatHelper, int lightmapId ); bool importEntities( const Q3BSP::Q3BSPModel *pModel, aiScene* pScene ); - bool expandFile( Q3BSP::Q3BSPZipArchive *pArchive, const std::string &rFilename, const std::vector &rExtList, + bool expandFile(ZipArchiveIOSystem *pArchive, const std::string &rFilename, const std::vector &rExtList, std::string &rFile, std::string &rExt ); private: diff --git a/code/Q3BSP/Q3BSPFileParser.cpp b/code/Q3BSP/Q3BSPFileParser.cpp index 5bac5ae76..bed2efe53 100644 --- a/code/Q3BSP/Q3BSPFileParser.cpp +++ b/code/Q3BSP/Q3BSPFileParser.cpp @@ -45,9 +45,9 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "Q3BSPFileParser.h" #include "Q3BSPFileData.h" -#include "Q3BSPZipArchive.h" #include #include +#include #include namespace Assimp { @@ -55,7 +55,7 @@ namespace Assimp { using namespace Q3BSP; // ------------------------------------------------------------------------------------------------ -Q3BSPFileParser::Q3BSPFileParser( const std::string &mapName, Q3BSPZipArchive *pZipArchive ) : +Q3BSPFileParser::Q3BSPFileParser( const std::string &mapName, ZipArchiveIOSystem *pZipArchive ) : m_sOffset( 0 ), m_Data(), m_pModel(nullptr), @@ -101,6 +101,7 @@ bool Q3BSPFileParser::readData( const std::string &rMapName ) { const size_t readSize = pMapFile->Read( &m_Data[0], sizeof( char ), size ); if ( readSize != size ) { m_Data.clear(); + m_pZipArchive->Close(pMapFile); return false; } m_pZipArchive->Close( pMapFile ); diff --git a/code/Q3BSP/Q3BSPFileParser.h b/code/Q3BSP/Q3BSPFileParser.h index 143e42b15..fd73f5e10 100644 --- a/code/Q3BSP/Q3BSPFileParser.h +++ b/code/Q3BSP/Q3BSPFileParser.h @@ -48,13 +48,12 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. namespace Assimp { + class ZipArchiveIOSystem; + namespace Q3BSP { - -class Q3BSPZipArchive; -struct Q3BSPModel; -class ZipFile; - + struct Q3BSPModel; + class ZipFile; } // ------------------------------------------------------------------- @@ -62,7 +61,7 @@ class ZipFile; class Q3BSPFileParser { public: - Q3BSPFileParser( const std::string &rMapName, Q3BSP::Q3BSPZipArchive *pZipArchive ); + Q3BSPFileParser( const std::string &rMapName, ZipArchiveIOSystem *pZipArchive ); ~Q3BSPFileParser(); Q3BSP::Q3BSPModel *getModel() const; @@ -83,7 +82,7 @@ private: size_t m_sOffset; std::vector m_Data; Q3BSP::Q3BSPModel *m_pModel; - Q3BSP::Q3BSPZipArchive *m_pZipArchive; + ZipArchiveIOSystem *m_pZipArchive; }; } // Namespace Assimp diff --git a/code/Q3BSP/Q3BSPZipArchive.cpp b/code/Q3BSP/Q3BSPZipArchive.cpp deleted file mode 100644 index 931f2b905..000000000 --- a/code/Q3BSP/Q3BSPZipArchive.cpp +++ /dev/null @@ -1,325 +0,0 @@ -/* -Open Asset Import Library (assimp) ----------------------------------------------------------------------- - -Copyright (c) 2006-2019, assimp team - - -All rights reserved. - -Redistribution and use of this software in source and binary forms, -with or without modification, are permitted provided that the -following conditions are met: - -* Redistributions of source code must retain the above - copyright notice, this list of conditions and the - following disclaimer. - -* Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the - following disclaimer in the documentation and/or other - materials provided with the distribution. - -* Neither the name of the assimp team, nor the names of its - contributors may be used to endorse or promote products - derived from this software without specific prior - written permission of the assimp team. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ----------------------------------------------------------------------- -*/ - -#ifndef ASSIMP_BUILD_NO_Q3BSP_IMPORTER - -#include "Q3BSPZipArchive.h" -#include -#include -#include - -namespace Assimp { -namespace Q3BSP { - -voidpf IOSystem2Unzip::open(voidpf opaque, const char* filename, int mode) { - IOSystem* io_system = (IOSystem*) opaque; - - const char* mode_fopen = NULL; - if((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER)==ZLIB_FILEFUNC_MODE_READ) { - mode_fopen = "rb"; - } else { - if(mode & ZLIB_FILEFUNC_MODE_EXISTING) { - mode_fopen = "r+b"; - } else { - if(mode & ZLIB_FILEFUNC_MODE_CREATE) { - mode_fopen = "wb"; - } - } - } - - return (voidpf) io_system->Open(filename, mode_fopen); -} - -uLong IOSystem2Unzip::read(voidpf /*opaque*/, voidpf stream, void* buf, uLong size) { - IOStream* io_stream = (IOStream*) stream; - - return static_cast(io_stream->Read(buf, 1, size)); -} - -uLong IOSystem2Unzip::write(voidpf /*opaque*/, voidpf stream, const void* buf, uLong size) { - IOStream* io_stream = (IOStream*) stream; - - return static_cast(io_stream->Write(buf, 1, size)); -} - -long IOSystem2Unzip::tell(voidpf /*opaque*/, voidpf stream) { - IOStream* io_stream = (IOStream*) stream; - - return static_cast(io_stream->Tell()); -} - -long IOSystem2Unzip::seek(voidpf /*opaque*/, voidpf stream, uLong offset, int origin) { - IOStream* io_stream = (IOStream*) stream; - - aiOrigin assimp_origin; - switch (origin) { - default: - case ZLIB_FILEFUNC_SEEK_CUR: - assimp_origin = aiOrigin_CUR; - break; - case ZLIB_FILEFUNC_SEEK_END: - assimp_origin = aiOrigin_END; - break; - case ZLIB_FILEFUNC_SEEK_SET: - assimp_origin = aiOrigin_SET; - break; - } - - return (io_stream->Seek(offset, assimp_origin) == aiReturn_SUCCESS ? 0 : -1); -} - -int IOSystem2Unzip::close(voidpf opaque, voidpf stream) { - IOSystem* io_system = (IOSystem*) opaque; - IOStream* io_stream = (IOStream*) stream; - - io_system->Close(io_stream); - - return 0; -} - -int IOSystem2Unzip::testerror(voidpf /*opaque*/, voidpf /*stream*/) { - return 0; -} - -zlib_filefunc_def IOSystem2Unzip::get(IOSystem* pIOHandler) { - zlib_filefunc_def mapping; - -#ifdef ASSIMP_USE_HUNTER - mapping.zopen_file = (open_file_func)open; - mapping.zread_file = (read_file_func)read; - mapping.zwrite_file = (write_file_func)write; - mapping.ztell_file = (tell_file_func)tell; - mapping.zseek_file = (seek_file_func)seek; - mapping.zclose_file = (close_file_func)close; - mapping.zerror_file = (error_file_func)testerror; -#else - mapping.zopen_file = open; - mapping.zread_file = read; - mapping.zwrite_file = write; - mapping.ztell_file = tell; - mapping.zseek_file = seek; - mapping.zclose_file = close; - mapping.zerror_file = testerror; -#endif - mapping.opaque = (voidpf) pIOHandler; - - return mapping; -} - -ZipFile::ZipFile(size_t size) : m_Size(size) { - ai_assert(m_Size != 0); - - m_Buffer = malloc(m_Size); -} - -ZipFile::~ZipFile() { - free(m_Buffer); - m_Buffer = NULL; -} - -size_t ZipFile::Read(void* pvBuffer, size_t pSize, size_t pCount) { - const size_t size = pSize * pCount; - assert(size <= m_Size); - - std::memcpy(pvBuffer, m_Buffer, size); - - return size; -} - -size_t ZipFile::Write(const void* /*pvBuffer*/, size_t /*pSize*/, size_t /*pCount*/) { - return 0; -} - -size_t ZipFile::FileSize() const { - return m_Size; -} - -aiReturn ZipFile::Seek(size_t /*pOffset*/, aiOrigin /*pOrigin*/) { - return aiReturn_FAILURE; -} - -size_t ZipFile::Tell() const { - return 0; -} - -void ZipFile::Flush() { - // empty -} - -// ------------------------------------------------------------------------------------------------ -// Constructor. -Q3BSPZipArchive::Q3BSPZipArchive(IOSystem* pIOHandler, const std::string& rFile) : m_ZipFileHandle(NULL), m_ArchiveMap() { - if (! rFile.empty()) { - zlib_filefunc_def mapping = IOSystem2Unzip::get(pIOHandler); - - m_ZipFileHandle = unzOpen2(rFile.c_str(), &mapping); - - if(m_ZipFileHandle != nullptr) { - mapArchive(); - } - } -} - -// ------------------------------------------------------------------------------------------------ -// Destructor. -Q3BSPZipArchive::~Q3BSPZipArchive() { - for(auto &file : m_ArchiveMap) { - delete file.second; - } - m_ArchiveMap.clear(); - - if(m_ZipFileHandle != nullptr) { - unzClose(m_ZipFileHandle); - m_ZipFileHandle = nullptr; - } -} - -// ------------------------------------------------------------------------------------------------ -// Returns true, if the archive is already open. -bool Q3BSPZipArchive::isOpen() const { - return (m_ZipFileHandle != nullptr); -} - -// ------------------------------------------------------------------------------------------------ -// Returns true, if the filename is part of the archive. -bool Q3BSPZipArchive::Exists(const char* pFile) const { - bool exist = false; - if (pFile != nullptr) { - std::string rFile(pFile); - std::map::const_iterator it = m_ArchiveMap.find(rFile); - - if(it != m_ArchiveMap.end()) { - exist = true; - } - } - - return exist; -} - -// ------------------------------------------------------------------------------------------------ -// Returns the separator delimiter. -char Q3BSPZipArchive::getOsSeparator() const { -#ifndef _WIN32 - return '/'; -#else - return '\\'; -#endif -} - -// ------------------------------------------------------------------------------------------------ -// Opens a file, which is part of the archive. -IOStream *Q3BSPZipArchive::Open(const char* pFile, const char* /*pMode*/) { - ai_assert(pFile != nullptr); - - IOStream* result = nullptr; - - std::map::iterator it = m_ArchiveMap.find(pFile); - - if(it != m_ArchiveMap.end()) { - result = (IOStream*) it->second; - } - - return result; -} - -// ------------------------------------------------------------------------------------------------ -// Close a filestream. -void Q3BSPZipArchive::Close(IOStream *pFile) { - (void)(pFile); - ai_assert(pFile != nullptr); - - // We don't do anything in case the file would be opened again in the future -} -// ------------------------------------------------------------------------------------------------ -// Returns the file-list of the archive. -void Q3BSPZipArchive::getFileList(std::vector &rFileList) { - rFileList.clear(); - - for(auto &file : m_ArchiveMap) { - rFileList.push_back(file.first); - } -} - -// ------------------------------------------------------------------------------------------------ -// Maps the archive content. -bool Q3BSPZipArchive::mapArchive() { - bool success = false; - - if(m_ZipFileHandle != nullptr) { - if(m_ArchiveMap.empty()) { - // At first ensure file is already open - if(unzGoToFirstFile(m_ZipFileHandle) == UNZ_OK) { - // Loop over all files - do { - char filename[FileNameSize]; - unz_file_info fileInfo; - - if(unzGetCurrentFileInfo(m_ZipFileHandle, &fileInfo, filename, FileNameSize, NULL, 0, NULL, 0) == UNZ_OK) { - // The file has EXACTLY the size of uncompressed_size. In C - // you need to mark the last character with '\0', so add - // another character - if(fileInfo.uncompressed_size != 0 && unzOpenCurrentFile(m_ZipFileHandle) == UNZ_OK) { - std::pair::iterator, bool> result = m_ArchiveMap.insert(std::make_pair(filename, new ZipFile(fileInfo.uncompressed_size))); - - if(unzReadCurrentFile(m_ZipFileHandle, result.first->second->m_Buffer, fileInfo.uncompressed_size) == (long int) fileInfo.uncompressed_size) { - if(unzCloseCurrentFile(m_ZipFileHandle) == UNZ_OK) { - // Nothing to do anymore... - } - } - } - } - } while(unzGoToNextFile(m_ZipFileHandle) != UNZ_END_OF_LIST_OF_FILE); - } - } - - success = true; - } - - return success; -} - -// ------------------------------------------------------------------------------------------------ - -} // Namespace Q3BSP -} // Namespace Assimp - -#endif // ASSIMP_BUILD_NO_Q3BSP_IMPORTER diff --git a/code/Q3BSP/Q3BSPZipArchive.h b/code/Q3BSP/Q3BSPZipArchive.h deleted file mode 100644 index db68925a8..000000000 --- a/code/Q3BSP/Q3BSPZipArchive.h +++ /dev/null @@ -1,135 +0,0 @@ -/* -Open Asset Import Library (assimp) ----------------------------------------------------------------------- - -Copyright (c) 2006-2019, assimp team - - -All rights reserved. - -Redistribution and use of this software in source and binary forms, -with or without modification, are permitted provided that the -following conditions are met: - -* Redistributions of source code must retain the above - copyright notice, this list of conditions and the - following disclaimer. - -* Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the - following disclaimer in the documentation and/or other - materials provided with the distribution. - -* Neither the name of the assimp team, nor the names of its - contributors may be used to endorse or promote products - derived from this software without specific prior - written permission of the assimp team. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ----------------------------------------------------------------------- -*/ -#ifndef AI_Q3BSP_ZIPARCHIVE_H_INC -#define AI_Q3BSP_ZIPARCHIVE_H_INC - -#ifdef ASSIMP_USE_HUNTER -# include -#else -# include -#endif -#include -#include -#include -#include -#include - -namespace Assimp { -namespace Q3BSP { - -// ------------------------------------------------------------------------------------------------ -/// \class IOSystem2Unzip -/// \ingroup Assimp::Q3BSP -/// -/// \brief -// ------------------------------------------------------------------------------------------------ -class IOSystem2Unzip { -public: - static voidpf open(voidpf opaque, const char* filename, int mode); - static uLong read(voidpf opaque, voidpf stream, void* buf, uLong size); - static uLong write(voidpf opaque, voidpf stream, const void* buf, uLong size); - static long tell(voidpf opaque, voidpf stream); - static long seek(voidpf opaque, voidpf stream, uLong offset, int origin); - static int close(voidpf opaque, voidpf stream); - static int testerror(voidpf opaque, voidpf stream); - static zlib_filefunc_def get(IOSystem* pIOHandler); -}; - -// ------------------------------------------------------------------------------------------------ -/// \class ZipFile -/// \ingroup Assimp::Q3BSP -/// -/// \brief -// ------------------------------------------------------------------------------------------------ -class ZipFile : public IOStream { - friend class Q3BSPZipArchive; - -public: - explicit ZipFile(size_t size); - ~ZipFile(); - size_t Read(void* pvBuffer, size_t pSize, size_t pCount ); - size_t Write(const void* /*pvBuffer*/, size_t /*pSize*/, size_t /*pCount*/); - size_t FileSize() const; - aiReturn Seek(size_t /*pOffset*/, aiOrigin /*pOrigin*/); - size_t Tell() const; - void Flush(); - -private: - void* m_Buffer; - size_t m_Size; -}; - -// ------------------------------------------------------------------------------------------------ -/// \class Q3BSPZipArchive -/// \ingroup Assimp::Q3BSP -/// -/// \brief IMplements a zip archive like the WinZip archives. Will be also used to import data -/// from a P3K archive ( Quake level format ). -// ------------------------------------------------------------------------------------------------ -class Q3BSPZipArchive : public Assimp::IOSystem { -public: - static const unsigned int FileNameSize = 256; - -public: - Q3BSPZipArchive(IOSystem* pIOHandler, const std::string & rFile); - ~Q3BSPZipArchive(); - bool Exists(const char* pFile) const; - char getOsSeparator() const; - IOStream* Open(const char* pFile, const char* pMode = "rb"); - void Close(IOStream* pFile); - bool isOpen() const; - void getFileList(std::vector &rFileList); - -private: - bool mapArchive(); - -private: - unzFile m_ZipFileHandle; - std::map m_ArchiveMap; -}; - -// ------------------------------------------------------------------------------------------------ - -} // Namespace Q3BSP -} // Namespace Assimp - -#endif // AI_Q3BSP_ZIPARCHIVE_H_INC From 9b78060a4aba5c04c919d8f8a2030703718f77d4 Mon Sep 17 00:00:00 2001 From: RichardTea <31507749+RichardTea@users.noreply.github.com> Date: Thu, 25 Jul 2019 10:14:42 +0100 Subject: [PATCH 12/19] Add copyright headers to ZipArchiveIOSystem --- code/Common/ZipArchiveIOSystem.cpp | 4 +++ include/assimp/ZipArchiveIOSystem.h | 47 +++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/code/Common/ZipArchiveIOSystem.cpp b/code/Common/ZipArchiveIOSystem.cpp index 43c66a921..7c37a05f9 100644 --- a/code/Common/ZipArchiveIOSystem.cpp +++ b/code/Common/ZipArchiveIOSystem.cpp @@ -40,6 +40,10 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---------------------------------------------------------------------- */ +/** @file ZipArchiveIOSystem.cpp + * @brief Zip File I/O implementation for #Importer + */ + #include #include diff --git a/include/assimp/ZipArchiveIOSystem.h b/include/assimp/ZipArchiveIOSystem.h index 7be597f59..38cbbf2a7 100644 --- a/include/assimp/ZipArchiveIOSystem.h +++ b/include/assimp/ZipArchiveIOSystem.h @@ -1,3 +1,50 @@ +/* +--------------------------------------------------------------------------- +Open Asset Import Library (assimp) +--------------------------------------------------------------------------- + +Copyright (c) 2006-2019, assimp team + + + +All rights reserved. + +Redistribution and use of this software in source and binary forms, +with or without modification, are permitted provided that the following +conditions are met: + +* Redistributions of source code must retain the above + copyright notice, this list of conditions and the + following disclaimer. + +* Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the + following disclaimer in the documentation and/or other + materials provided with the distribution. + +* Neither the name of the assimp team, nor the names of its + contributors may be used to endorse or promote products + derived from this software without specific prior + written permission of the assimp team. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +--------------------------------------------------------------------------- +*/ + +/** @file ZipArchiveIOSystem.h + * @brief Implementation of IOSystem to read a ZIP file from another IOSystem +*/ + #ifndef AI_ZIPARCHIVEIOSYSTEM_H_INC #define AI_ZIPARCHIVEIOSYSTEM_H_INC From 9dce8e40ba045aa3f5673a608ec0d6ff6c8cc030 Mon Sep 17 00:00:00 2001 From: Pierre Anquez <32702237+panquez@users.noreply.github.com> Date: Thu, 25 Jul 2019 16:03:26 +0200 Subject: [PATCH 13/19] Update CMakeLists.txt --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ad0462531..412628bbe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -341,7 +341,7 @@ SET( ASSIMP_BIN_INSTALL_DIR "bin" CACHE STRING get_cmake_property(is_multi_config GENERATOR_IS_MULTI_CONFIG) -IF (is_multi_config OR (CMAKE_BUILD_TYPE STREQUAL "Debug")) +IF (INJECT_DEBUG_POSTFIX AND (CMAKE_BUILD_TYPE STREQUAL "Debug")) SET(CMAKE_DEBUG_POSTFIX "d" CACHE STRING "Debug Postfix for lib, samples and tools") ELSE() SET(CMAKE_DEBUG_POSTFIX "" CACHE STRING "Debug Postfix for lib, samples and tools") From a879102309cc329db0dd8d1193aca5b021a3316c Mon Sep 17 00:00:00 2001 From: Pierre Anquez <32702237+panquez@users.noreply.github.com> Date: Thu, 25 Jul 2019 16:27:35 +0200 Subject: [PATCH 14/19] Update CMakeLists.txt --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 412628bbe..3e0f55df0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -341,8 +341,8 @@ SET( ASSIMP_BIN_INSTALL_DIR "bin" CACHE STRING get_cmake_property(is_multi_config GENERATOR_IS_MULTI_CONFIG) -IF (INJECT_DEBUG_POSTFIX AND (CMAKE_BUILD_TYPE STREQUAL "Debug")) - SET(CMAKE_DEBUG_POSTFIX "d" CACHE STRING "Debug Postfix for lib, samples and tools") +IF (CMAKE_BUILD_TYPE STREQUAL "Debug") + SET(CMAKE_DEBUG_POSTFIX "totototot" CACHE STRING "Debug Postfix for lib, samples and tools") ELSE() SET(CMAKE_DEBUG_POSTFIX "" CACHE STRING "Debug Postfix for lib, samples and tools") ENDIF() From 80047cd633e53dfd57015cad5bbdae067f61387a Mon Sep 17 00:00:00 2001 From: Pierre Anquez <32702237+panquez@users.noreply.github.com> Date: Thu, 25 Jul 2019 16:29:17 +0200 Subject: [PATCH 15/19] Update Exporter.cpp --- code/Common/Exporter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/Common/Exporter.cpp b/code/Common/Exporter.cpp index 58cad3be8..87745bca9 100644 --- a/code/Common/Exporter.cpp +++ b/code/Common/Exporter.cpp @@ -183,7 +183,7 @@ Exporter::ExportFormatEntry gExporters[] = Exporter::ExportFormatEntry( "3mf", "The 3MF-File-Format", "3mf", &ExportScene3MF, 0 ), #endif -#ifndef ASSIMP_BUILD_NO_ASSJSON_EXPORTER +#ifndef ASSIMP_BUILD_NO_Assjson_EXPORTER Exporter::ExportFormatEntry("json", "Plain JSON representation of the Assimp scene data structure", "json", &ExportAssimp2Json, 0) #endif }; From 337df2020972fe8e91e30683151e0cf5475e4249 Mon Sep 17 00:00:00 2001 From: Pierre Anquez <32702237+panquez@users.noreply.github.com> Date: Thu, 25 Jul 2019 17:12:16 +0200 Subject: [PATCH 16/19] Update CMakeLists.txt --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3e0f55df0..412628bbe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -341,8 +341,8 @@ SET( ASSIMP_BIN_INSTALL_DIR "bin" CACHE STRING get_cmake_property(is_multi_config GENERATOR_IS_MULTI_CONFIG) -IF (CMAKE_BUILD_TYPE STREQUAL "Debug") - SET(CMAKE_DEBUG_POSTFIX "totototot" CACHE STRING "Debug Postfix for lib, samples and tools") +IF (INJECT_DEBUG_POSTFIX AND (CMAKE_BUILD_TYPE STREQUAL "Debug")) + SET(CMAKE_DEBUG_POSTFIX "d" CACHE STRING "Debug Postfix for lib, samples and tools") ELSE() SET(CMAKE_DEBUG_POSTFIX "" CACHE STRING "Debug Postfix for lib, samples and tools") ENDIF() From cfbe368d72cec6f3dd42ac164c4de08ed728837e Mon Sep 17 00:00:00 2001 From: Minmin Gong Date: Mon, 29 Jul 2019 22:33:42 -0700 Subject: [PATCH 17/19] Remove the usage of std::iterator --- code/X3D/X3DImporter.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/code/X3D/X3DImporter.cpp b/code/X3D/X3DImporter.cpp index 1f4d3f916..96fcf067a 100644 --- a/code/X3D/X3DImporter.cpp +++ b/code/X3D/X3DImporter.cpp @@ -80,7 +80,13 @@ const aiImporterDesc X3DImporter::Description = { //const std::regex X3DImporter::pattern_nws(R"([^, \t\r\n]+)"); //const std::regex X3DImporter::pattern_true(R"(^\s*(?:true|1)\s*$)", std::regex::icase); -struct WordIterator: public std::iterator { +struct WordIterator { + using iterator_category = std::input_iterator_tag; + using value_type = const char*; + using difference_type = ptrdiff_t; + using pointer = value_type*; + using reference = value_type&; + static const char *whitespace; const char *start_, *end_; WordIterator(const char *start, const char *end): start_(start), end_(end) { From 9330cca1cd0fbfcdb1c81202165cabb9b958d5d7 Mon Sep 17 00:00:00 2001 From: petrmohelnik Date: Sat, 3 Aug 2019 20:51:00 +0200 Subject: [PATCH 18/19] glTF 2.0 Lights import Importing of lights according to glTF 2.0 extension KHR_lights_punctual https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_lights_punctual Since glTF lights are based on PBR they use different attenuation model than conventional lights supported by assimp. It is possible to use attenuation factors in assimp to describe inverse square law fallof. But the light structure does not provide means to save range property. Therefore I resorted to use of metadata. When range parameter is present, I put it into 'PBR_LightRange' metadata of light's node. Please, see comment in glTF2Importer file. --- code/glTF2/glTF2Asset.h | 27 +++++++++++ code/glTF2/glTF2Asset.inl | 47 ++++++++++++++++++ code/glTF2/glTF2AssetWriter.inl | 5 ++ code/glTF2/glTF2Importer.cpp | 84 +++++++++++++++++++++++++++++++-- 4 files changed, 159 insertions(+), 4 deletions(-) diff --git a/code/glTF2/glTF2Asset.h b/code/glTF2/glTF2Asset.h index 70f92df5b..23015c90a 100644 --- a/code/glTF2/glTF2Asset.h +++ b/code/glTF2/glTF2Asset.h @@ -46,6 +46,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * glTF Extensions Support: * KHR_materials_pbrSpecularGlossiness full * KHR_materials_unlit full + * KHR_lights_punctual full */ #ifndef GLTF2ASSET_H_INC #define GLTF2ASSET_H_INC @@ -668,6 +669,28 @@ namespace glTF2 void Read(Value& obj, Asset& r); }; + //! A light (from KHR_lights_punctual extension) + struct Light : public Object + { + enum Type + { + Directional, + Point, + Spot + }; + + Type type; + + vec3 color; + float intensity; + Nullable range; + + float innerConeAngle; + float outerConeAngle; + + Light() {} + void Read(Value& obj, Asset& r); + }; //! Image data used to create a texture. struct Image : public Object @@ -819,6 +842,7 @@ namespace glTF2 Nullable scale; Ref camera; + Ref light; std::vector< Ref > skeletons; //!< The ID of skeleton nodes. Each of which is the root of a node hierarchy. Ref skin; //!< The ID of the skin referenced by this node. @@ -1050,6 +1074,7 @@ namespace glTF2 { bool KHR_materials_pbrSpecularGlossiness; bool KHR_materials_unlit; + bool KHR_lights_punctual; } extensionsUsed; @@ -1063,6 +1088,7 @@ namespace glTF2 LazyDict buffers; LazyDict bufferViews; LazyDict cameras; + LazyDict lights; LazyDict images; LazyDict materials; LazyDict meshes; @@ -1083,6 +1109,7 @@ namespace glTF2 , buffers (*this, "buffers") , bufferViews (*this, "bufferViews") , cameras (*this, "cameras") + , lights (*this, "lights", "KHR_lights_punctual") , images (*this, "images") , materials (*this, "materials") , meshes (*this, "meshes") diff --git a/code/glTF2/glTF2Asset.inl b/code/glTF2/glTF2Asset.inl index b51975c77..6f9eba50b 100644 --- a/code/glTF2/glTF2Asset.inl +++ b/code/glTF2/glTF2Asset.inl @@ -1067,6 +1067,39 @@ inline void Camera::Read(Value& obj, Asset& /*r*/) } } +inline void Light::Read(Value& obj, Asset& /*r*/) +{ +#ifndef M_PI + const float M_PI = 3.14159265358979323846f; +#endif + + std::string type_string; + ReadMember(obj, "type", type_string); + if (type_string == "directional") + type = Light::Directional; + else if (type_string == "point") + type = Light::Point; + else + type = Light::Spot; + + name = MemberOrDefault(obj, "name", ""); + + SetVector(color, vec3{ 1.0f, 1.0f, 1.0f }); + ReadMember(obj, "color", color); + + intensity = MemberOrDefault(obj, "intensity", 1.0f); + + ReadMember(obj, "range", range); + + if (type == Light::Spot) + { + Value* spot = FindObject(obj, "spot"); + if (!spot) throw DeadlyImportError("GLTF: Light missing its spot parameters"); + innerConeAngle = MemberOrDefault(*spot, "innerConeAngle", 0.0f); + outerConeAngle = MemberOrDefault(*spot, "outerConeAngle", M_PI / 4.0f); + } +} + inline void Node::Read(Value& obj, Asset& r) { @@ -1110,6 +1143,19 @@ inline void Node::Read(Value& obj, Asset& r) if (this->camera) this->camera->id = this->id; } + + if (Value* extensions = FindObject(obj, "extensions")) { + if (r.extensionsUsed.KHR_lights_punctual) { + + if (Value* ext = FindObject(*extensions, "KHR_lights_punctual")) { + if (Value* light = FindUInt(*ext, "light")) { + this->light = r.lights.Retrieve(light->GetUint()); + if (this->light) + this->light->id = this->id; + } + } + } + } } inline void Scene::Read(Value& obj, Asset& r) @@ -1421,6 +1467,7 @@ inline void Asset::ReadExtensionsUsed(Document& doc) CHECK_EXT(KHR_materials_pbrSpecularGlossiness); CHECK_EXT(KHR_materials_unlit); + CHECK_EXT(KHR_lights_punctual); #undef CHECK_EXT } diff --git a/code/glTF2/glTF2AssetWriter.inl b/code/glTF2/glTF2AssetWriter.inl index bec88ceb8..92168fa61 100644 --- a/code/glTF2/glTF2AssetWriter.inl +++ b/code/glTF2/glTF2AssetWriter.inl @@ -202,6 +202,11 @@ namespace glTF2 { } + inline void Write(Value& /*obj*/, Light& /*c*/, AssetWriter& /*w*/) + { + + } + inline void Write(Value& obj, Image& img, AssetWriter& w) { if (img.bufferView) { diff --git a/code/glTF2/glTF2Importer.cpp b/code/glTF2/glTF2Importer.cpp index a2b18fc49..c6e998b3a 100644 --- a/code/glTF2/glTF2Importer.cpp +++ b/code/glTF2/glTF2Importer.cpp @@ -140,10 +140,10 @@ static aiTextureMapMode ConvertWrappingMode(SamplerWrap gltfWrapMode) } } -//static void CopyValue(const glTF2::vec3& v, aiColor3D& out) -//{ -// out.r = v[0]; out.g = v[1]; out.b = v[2]; -//} +static void CopyValue(const glTF2::vec3& v, aiColor3D& out) +{ + out.r = v[0]; out.g = v[1]; out.b = v[2]; +} static void CopyValue(const glTF2::vec4& v, aiColor4D& out) { @@ -710,6 +710,69 @@ void glTF2Importer::ImportCameras(glTF2::Asset& r) } } +void glTF2Importer::ImportLights(glTF2::Asset& r) +{ + if (!r.lights.Size()) + return; + + mScene->mNumLights = r.lights.Size(); + mScene->mLights = new aiLight*[r.lights.Size()]; + + for (size_t i = 0; i < r.lights.Size(); ++i) { + Light& light = r.lights[i]; + + aiLight* ail = mScene->mLights[i] = new aiLight(); + + switch (light.type) + { + case Light::Directional: + ail->mType = aiLightSource_DIRECTIONAL; break; + case Light::Point: + ail->mType = aiLightSource_POINT; break; + case Light::Spot: + ail->mType = aiLightSource_SPOT; break; + } + + if (ail->mType != aiLightSource_POINT) + { + ail->mDirection = aiVector3D(0.0f, 0.0f, -1.0f); + ail->mUp = aiVector3D(0.0f, 1.0f, 0.0f); + } + + vec3 colorWithIntensity = { light.color[0] * light.intensity, light.color[1] * light.intensity, light.color[2] * light.intensity }; + CopyValue(colorWithIntensity, ail->mColorAmbient); + CopyValue(colorWithIntensity, ail->mColorDiffuse); + CopyValue(colorWithIntensity, ail->mColorSpecular); + + if (ail->mType == aiLightSource_DIRECTIONAL) + { + ail->mAttenuationConstant = 1.0; + ail->mAttenuationLinear = 0.0; + ail->mAttenuationQuadratic = 0.0; + } + else + { + //in PBR attenuation is calculated using inverse square law which can be expressed + //using assimps equation: 1/(att0 + att1 * d + att2 * d*d) with the following parameters + //this is correct equation for the case when range (see + //https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_lights_punctual) + //is not present. When range is not present it is assumed that it is infinite and so numerator is 1. + //When range is present then numerator might be any value in range [0,1] and then assimps equation + //will not suffice. In this case range is added into metadata in ImportNode function + //and its up to implementation to read it when it wants to + ail->mAttenuationConstant = 0.0; + ail->mAttenuationLinear = 0.0; + ail->mAttenuationQuadratic = 1.0; + } + + if (ail->mType == aiLightSource_SPOT) + { + ail->mAngleInnerCone = light.innerConeAngle; + ail->mAngleOuterCone = light.outerConeAngle; + } + } +} + static void GetNodeTransform(aiMatrix4x4& matrix, const glTF2::Node& node) { if (node.matrix.isPresent) { CopyValue(node.matrix.value, matrix); @@ -881,6 +944,18 @@ aiNode* ImportNode(aiScene* pScene, glTF2::Asset& r, std::vector& pScene->mCameras[node.camera.GetIndex()]->mName = ainode->mName; } + if (node.light) { + pScene->mLights[node.light.GetIndex()]->mName = ainode->mName; + + //range is optional - see https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_lights_punctual + //it is added to meta data of parent node, because there is no other place to put it + if (node.light->range.isPresent) + { + ainode->mMetaData = aiMetadata::Alloc(1); + ainode->mMetaData->Set(0, "PBR_LightRange", node.light->range.value); + } + } + return ainode; } @@ -1150,6 +1225,7 @@ void glTF2Importer::InternReadFile(const std::string& pFile, aiScene* pScene, IO ImportMeshes(asset); ImportCameras(asset); + ImportLights(asset); ImportNodes(asset); From 4c1e8fe9c3b61d9fca5814e1c038985e83b3449a Mon Sep 17 00:00:00 2001 From: Kim Kulling Date: Fri, 9 Aug 2019 20:30:12 +0200 Subject: [PATCH 19/19] Update config.h.in closes https://github.com/assimp/assimp/issues/2570: fix a typo. --- include/assimp/config.h.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/assimp/config.h.in b/include/assimp/config.h.in index d08b929a1..dad43710e 100644 --- a/include/assimp/config.h.in +++ b/include/assimp/config.h.in @@ -142,7 +142,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. /** @brief Specifies the maximum angle that may be between two vertex tangents * that their tangents and bi-tangents are smoothed. * - * This applies to the CalcTangentSpace-Step. TFvhe angle is specified + * This applies to the CalcTangentSpace-Step. The angle is specified * in degrees. The maximum value is 175. * Property type: float. Default value: 45 degrees */