From d952823ffd7920b0110980b6029c2b3b7bb5c487 Mon Sep 17 00:00:00 2001 From: rmitton Date: Sun, 13 Dec 2015 22:14:19 -0800 Subject: [PATCH] Importer for Silo SIB files. Wrote an importer for Nevercenter Silo's binary SIB model format --- code/3DSLoader.cpp | 4 +- code/CMakeLists.txt | 5 + code/ImporterRegistry.cpp | 6 + code/SIBImporter.cpp | 919 ++++++++++++++++++++++++++++++++ code/SIBImporter.h | 119 +++++ code/StreamReader.h | 12 +- test/models/SIB/This Way Up.png | Bin 0 -> 5922 bytes test/models/SIB/UV Mapping.png | Bin 0 -> 7069 bytes test/models/SIB/heffalump.sib | Bin 0 -> 55682 bytes test/models/SIB/readme.txt | 3 + 10 files changed, 1061 insertions(+), 7 deletions(-) create mode 100644 code/SIBImporter.cpp create mode 100644 code/SIBImporter.h create mode 100644 test/models/SIB/This Way Up.png create mode 100644 test/models/SIB/UV Mapping.png create mode 100644 test/models/SIB/heffalump.sib create mode 100644 test/models/SIB/readme.txt diff --git a/code/3DSLoader.cpp b/code/3DSLoader.cpp index 3c80c320b..5582864cb 100644 --- a/code/3DSLoader.cpp +++ b/code/3DSLoader.cpp @@ -86,8 +86,8 @@ static const aiImporterDesc desc = { int chunkSize = chunk.Size-sizeof(Discreet3DS::Chunk); \ if(chunkSize <= 0) \ continue; \ - const int oldReadLimit = stream->GetReadLimit(); \ - stream->SetReadLimit(stream->GetCurrentPos() + chunkSize); \ + const unsigned int oldReadLimit = stream->SetReadLimit( \ + stream->GetCurrentPos() + chunkSize); \ // ------------------------------------------------------------------------------------------------ diff --git a/code/CMakeLists.txt b/code/CMakeLists.txt index dd15e3ce4..a568ea8b1 100644 --- a/code/CMakeLists.txt +++ b/code/CMakeLists.txt @@ -530,6 +530,11 @@ ADD_ASSIMP_IMPORTER(RAW RawLoader.h ) +ADD_ASSIMP_IMPORTER(SIB + SIBImporter.cpp + SIBImporter.h +) + ADD_ASSIMP_IMPORTER(SMD SMDLoader.cpp SMDLoader.h diff --git a/code/ImporterRegistry.cpp b/code/ImporterRegistry.cpp index f8382bcdc..ba1ea6337 100644 --- a/code/ImporterRegistry.cpp +++ b/code/ImporterRegistry.cpp @@ -101,6 +101,9 @@ corresponding preprocessor flag to selectively disable formats. #ifndef ASSIMP_BUILD_NO_RAW_IMPORTER # include "RawLoader.h" #endif +#ifndef ASSIMP_BUILD_NO_SIB_IMPORTER +# include "SIBImporter.h" +#endif #ifndef ASSIMP_BUILD_NO_OFF_IMPORTER # include "OFFLoader.h" #endif @@ -238,6 +241,9 @@ void GetImporterInstanceList(std::vector< BaseImporter* >& out) #if (!defined ASSIMP_BUILD_NO_RAW_IMPORTER) out.push_back( new RAWImporter()); #endif +#if (!defined ASSIMP_BUILD_NO_SIB_IMPORTER) + out.push_back( new SIBImporter()); +#endif #if (!defined ASSIMP_BUILD_NO_OFF_IMPORTER) out.push_back( new OFFImporter()); #endif diff --git a/code/SIBImporter.cpp b/code/SIBImporter.cpp new file mode 100644 index 000000000..bfb093512 --- /dev/null +++ b/code/SIBImporter.cpp @@ -0,0 +1,919 @@ +/* +--------------------------------------------------------------------------- +Open Asset Import Library (assimp) +--------------------------------------------------------------------------- + +Copyright (c) 2006-2015, 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 SIBImporter.cpp + * @brief Implementation of the SIB importer class + * + * The Nevercenter Silo SIB format is undocumented. + * All details here have been reverse engineered from + * studying the binary files output by Silo. + * + * Nevertheless, this implementation is reasonably complete. + */ + + +#ifndef ASSIMP_BUILD_NO_SIB_IMPORTER + +// internal headers +#include "SIBImporter.h" +#include "ByteSwapper.h" +#include "StreamReader.h" +#include "TinyFormatter.h" +#include "../contrib/ConvertUTF/ConvertUTF.h" +#include "../include/assimp/IOSystem.hpp" +#include "../include/assimp/DefaultLogger.hpp" +#include "../include/assimp/scene.h" + + +using namespace Assimp; + +static const aiImporterDesc desc = { + "Silo SIB Importer", + "Richard Mitton (http://www.codersnotes.com/about)", + "", + "Does not apply subdivision.", + aiImporterFlags_SupportBinaryFlavour, + 0, 0, + 0, 0, + "sib" +}; + +struct SIBChunk +{ + uint32_t Tag; + uint32_t Size; +} PACK_STRUCT; + +enum { POS, NRM, UV, N }; + +typedef std::pair SIBPair; +static SIBPair makePair(uint32_t a, uint32_t b) { return (a pos, nrm, uv; + std::vector idx; + std::vector faceStart; + std::vector mtls; + std::vector edges; + std::map edgeMap; +}; + +struct SIBObject +{ + aiString name; + aiMatrix4x4 axis; + size_t meshIdx, meshCount; +}; + +struct SIB +{ + std::vector mtls; + std::vector meshes; + std::vector lights; + std::vector objs, insts; +}; + +// ------------------------------------------------------------------------------------------------ +static SIBEdge& GetEdge(SIBMesh* mesh, uint32_t posA, uint32_t posB) +{ + SIBPair pair = (posA < posB) ? SIBPair(posA, posB) : SIBPair(posB, posA); + std::map::iterator it = mesh->edgeMap.find(pair); + if (it != mesh->edgeMap.end()) + return mesh->edges[it->second]; + + SIBEdge edge; + edge.creased = false; + edge.faceA = edge.faceB = 0xffffffff; + mesh->edgeMap[pair] = mesh->edges.size(); + mesh->edges.push_back(edge); + return mesh->edges.back(); +} + +// ------------------------------------------------------------------------------------------------ +// Helpers for reading chunked data. + +#define TAG(A,B,C,D) ((A << 24) | (B << 16) | (C << 8) | D) + +static SIBChunk ReadChunk(StreamReaderLE* stream) +{ + SIBChunk chunk; + chunk.Tag = stream->GetU4(); + chunk.Size = stream->GetU4(); + if (chunk.Size > stream->GetRemainingSizeToLimit()) + DefaultLogger::get()->error("SIB: Chunk overflow"); + ByteSwap::Swap4(&chunk.Tag); + return chunk; +} + +static aiColor3D ReadColor(StreamReaderLE* stream) +{ + float r = stream->GetF4(); + float g = stream->GetF4(); + float b = stream->GetF4(); + stream->GetU4(); // Colors have an unused(?) 4th component. + return aiColor3D(r, g, b); +} + +static void UnknownChunk(StreamReaderLE* stream, const SIBChunk& chunk) +{ + char temp[5] = { (chunk.Tag>>24)&0xff, (chunk.Tag>>16)&0xff, (chunk.Tag>>8)&0xff, chunk.Tag&0xff, '\0' }; + + DefaultLogger::get()->warn((Formatter::format(), "SIB: Skipping unknown '",temp,"' chunk.")); +} + +// Reads a UTF-16LE string and returns it at UTF-8. +static aiString ReadString(StreamReaderLE* stream, uint32_t numWChars) +{ + // Allocate buffers (max expansion is 1 byte -> 4 bytes for UTF-8) + UTF16* temp = new UTF16[numWChars]; + UTF8* str = new UTF8[numWChars * 4 + 1]; + for (uint32_t n=0;nGetU2(); + + // Convert it and NUL-terminate. + const UTF16 *start = temp, *end = temp + numWChars; + UTF8 *dest = str, *limit = str + numWChars*4; + ConvertUTF16toUTF8(&start, end, &dest, limit, lenientConversion); + *dest = '\0'; + + // Return the final string. + aiString result = aiString((const char *)str); + delete[] str; + delete[] temp; + return result; +} + +// ------------------------------------------------------------------------------------------------ +// Constructor to be privately used by Importer +SIBImporter::SIBImporter() +{} + +// ------------------------------------------------------------------------------------------------ +// Destructor, private as well +SIBImporter::~SIBImporter() +{} + +// ------------------------------------------------------------------------------------------------ +// Returns whether the class can handle the format of the given file. +bool SIBImporter::CanRead( const std::string& pFile, IOSystem* /*pIOHandler*/, bool /*checkSig*/) const +{ + return SimpleExtensionCheck(pFile, "sib"); +} + +// ------------------------------------------------------------------------------------------------ +const aiImporterDesc* SIBImporter::GetInfo () const +{ + return &desc; +} + +// ------------------------------------------------------------------------------------------------ +static void ReadVerts(SIBMesh* mesh, StreamReaderLE* stream, uint32_t count) +{ + mesh->pos.resize(count); + + for (uint32_t n=0;npos[n].x = stream->GetF4(); + mesh->pos[n].y = stream->GetF4(); + mesh->pos[n].z = stream->GetF4(); + } +} + +// ------------------------------------------------------------------------------------------------ +static void ReadFaces(SIBMesh* mesh, StreamReaderLE* stream) +{ + uint32_t ptIdx = 0; + while (stream->GetRemainingSizeToLimit() > 0) + { + uint32_t numPoints = stream->GetU4(); + + // Store room for the N index channels, plus the point count. + size_t pos = mesh->idx.size() + 1; + mesh->idx.resize(pos + numPoints*N); + mesh->idx[pos-1] = numPoints; + uint32_t *idx = &mesh->idx[pos]; + + mesh->faceStart.push_back(pos-1); + mesh->mtls.push_back(0); + + // Read all the position data. + // UV/normals will be supplied later. + // Positions are supplied indexed already, so we preserve that + // mapping. UVs are supplied uniquely, so we allocate unique indices. + for (uint32_t n=0;nGetU4(); + if (p >= mesh->pos.size()) + throw DeadlyImportError("Vertex index is out of range."); + idx[POS] = p; + idx[NRM] = ptIdx; + idx[UV] = ptIdx; + } + } + + // Allocate data channels for normals/UVs. + mesh->nrm.resize(ptIdx, aiVector3D(0,0,0)); + mesh->uv.resize(ptIdx, aiVector3D(0,0,0)); + + mesh->numPts = ptIdx; +} + +// ------------------------------------------------------------------------------------------------ +static void ReadUVs(SIBMesh* mesh, StreamReaderLE* stream) +{ + while (stream->GetRemainingSizeToLimit() > 0) + { + uint32_t faceIdx = stream->GetU4(); + uint32_t numPoints = stream->GetU4(); + + if (faceIdx >= mesh->faceStart.size()) + throw DeadlyImportError("Invalid face index."); + + uint32_t pos = mesh->faceStart[faceIdx]; + uint32_t *idx = &mesh->idx[pos + 1]; + + for (uint32_t n=0;nuv[id].x = stream->GetF4(); + mesh->uv[id].y = stream->GetF4(); + } + } +} + +// ------------------------------------------------------------------------------------------------ +static void ReadMtls(SIBMesh* mesh, StreamReaderLE* stream) +{ + // Material assignments are stored run-length encoded. + // Also, we add 1 to each material so that we can use mtl #0 + // as the default material. + uint32_t prevFace = stream->GetU4(); + uint32_t prevMtl = stream->GetU4() + 1; + while (stream->GetRemainingSizeToLimit() > 0) + { + uint32_t face = stream->GetU4(); + uint32_t mtl = stream->GetU4() + 1; + while (prevFace < face) + { + if (prevFace >= mesh->mtls.size()) + throw DeadlyImportError("Invalid face index."); + mesh->mtls[prevFace++] = prevMtl; + } + + prevFace = face; + prevMtl = mtl; + } + + while (prevFace < mesh->mtls.size()) + mesh->mtls[prevFace++] = prevMtl; +} + +// ------------------------------------------------------------------------------------------------ +static void ReadAxis(aiMatrix4x4& axis, StreamReaderLE* stream) +{ + axis.a4 = stream->GetF4(); + axis.b4 = stream->GetF4(); + axis.c4 = stream->GetF4(); + axis.d4 = 1; + axis.a1 = stream->GetF4(); + axis.b1 = stream->GetF4(); + axis.c1 = stream->GetF4(); + axis.d1 = 0; + axis.a2 = stream->GetF4(); + axis.b2 = stream->GetF4(); + axis.c2 = stream->GetF4(); + axis.d2 = 0; + axis.a3 = stream->GetF4(); + axis.b3 = stream->GetF4(); + axis.c3 = stream->GetF4(); + axis.d3 = 0; +} + +// ------------------------------------------------------------------------------------------------ +static void ReadEdges(SIBMesh* mesh, StreamReaderLE* stream) +{ + while (stream->GetRemainingSizeToLimit() > 0) + { + uint32_t posA = stream->GetU4(); + uint32_t posB = stream->GetU4(); + GetEdge(mesh, posA, posB); + } +} + +// ------------------------------------------------------------------------------------------------ +static void ReadCreases(SIBMesh* mesh, StreamReaderLE* stream) +{ + while (stream->GetRemainingSizeToLimit() > 0) + { + uint32_t edge = stream->GetU4(); + if (edge >= mesh->edges.size()) + throw DeadlyImportError("SIB: Invalid edge index."); + mesh->edges[edge].creased = true; + } +} + +// ------------------------------------------------------------------------------------------------ +static void ConnectFaces(SIBMesh* mesh) +{ + // Find faces connected to each edge. + size_t numFaces = mesh->faceStart.size(); + for (size_t faceIdx=0;faceIdxidx[mesh->faceStart[faceIdx]]; + uint32_t numPoints = *idx++; + uint32_t prev = idx[(numPoints-1)*N+POS]; + + for (uint32_t i=0;i& faceNormals) +{ + // Creased edges complicate this. We need to find the start/end range of the + // ring of faces that touch this position. + // We do this in two passes. The first pass is to find the end of the range, + // the second is to work backwards to the start and calculate the final normal. + aiVector3D vtxNormal; + for (int pass=0;pass<2;pass++) + { + vtxNormal = aiVector3D(0, 0, 0); + uint32_t startFaceIdx = faceIdx; + uint32_t prevFaceIdx = faceIdx; + + // Process each connected face. + while (true) + { + // Accumulate the face normal. + vtxNormal += faceNormals[faceIdx]; + + uint32_t nextFaceIdx = 0xffffffff; + + // Move to the next edge sharing this position. + uint32_t* idx = &mesh->idx[mesh->faceStart[faceIdx]]; + uint32_t numPoints = *idx++; + uint32_t posA = idx[(numPoints-1)*N+POS]; + for (uint32_t n=0;n 0.000000001f) + vtxNormal /= len; + return vtxNormal; +} + +// ------------------------------------------------------------------------------------------------ +static void CalculateNormals(SIBMesh* mesh) +{ + size_t numFaces = mesh->faceStart.size(); + + // Calculate face normals. + std::vector faceNormals(numFaces); + for (size_t faceIdx=0;faceIdxidx[mesh->faceStart[faceIdx]]; + uint32_t numPoints = *idx++; + + aiVector3D faceNormal(0, 0, 0); + + uint32_t *prev = &idx[(numPoints-1)*N]; + + for (uint32_t i=0;ipos[prev[POS]] ^ mesh->pos[next[POS]]; + prev = next; + } + + faceNormals[faceIdx] = faceNormal; + } + + // Calculate vertex normals. + for (size_t faceIdx=0;faceIdxidx[mesh->faceStart[faceIdx]]; + uint32_t numPoints = *idx++; + + for (uint32_t i=0;inrm[nrm] = vtxNorm; + } + } +} + +// ------------------------------------------------------------------------------------------------ +struct TempMesh +{ + std::vector vtx; + std::vector nrm; + std::vector uv; + std::vector faces; +}; + +static void ReadShape(SIB* sib, StreamReaderLE* stream) +{ + SIBMesh smesh; + aiString name; + + while (stream->GetRemainingSizeToLimit() >= sizeof(SIBChunk)) + { + SIBChunk chunk = ReadChunk(stream); + unsigned oldLimit = stream->SetReadLimit(stream->GetCurrentPos() + chunk.Size); + + switch (chunk.Tag) + { + case TAG('M','I','R','P'): break; // mirror plane maybe? + case TAG('I','M','R','P'): break; // instance mirror? (not supported here yet) + case TAG('D','I','N','F'): break; // display info, not needed + case TAG('P','I','N','F'): break; // ? + case TAG('V','M','I','R'): break; // ? + case TAG('F','M','I','R'): break; // ? + case TAG('T','X','S','M'): break; // ? + case TAG('F','A','H','S'): break; // ? + case TAG('V','R','T','S'): ReadVerts(&smesh, stream, chunk.Size/12); break; + case TAG('F','A','C','S'): ReadFaces(&smesh, stream); break; + case TAG('F','T','V','S'): ReadUVs(&smesh, stream); break; + case TAG('S','N','A','M'): name = ReadString(stream, chunk.Size/2); break; + case TAG('F','A','M','A'): ReadMtls(&smesh, stream); break; + case TAG('A','X','I','S'): ReadAxis(smesh.axis, stream); break; + case TAG('E','D','G','S'): ReadEdges(&smesh, stream); break; + case TAG('E','C','R','S'): ReadCreases(&smesh, stream); break; + default: UnknownChunk(stream, chunk); break; + } + + stream->SetCurrentPos(stream->GetReadLimit()); + stream->SetReadLimit(oldLimit); + } + + assert(smesh.faceStart.size() == smesh.mtls.size()); // sanity check + + // Silo doesn't store any normals in the file - we need to compute + // them ourselves. We can't let AssImp handle it as AssImp doesn't + // know about our creased edges. + ConnectFaces(&smesh); + CalculateNormals(&smesh); + + // Construct the transforms. + aiMatrix4x4 worldToLocal = smesh.axis; + worldToLocal.Inverse(); + aiMatrix4x4 worldToLocalN = worldToLocal; + worldToLocalN.a4 = worldToLocalN.b4 = worldToLocalN.c4 = 0.0f; + worldToLocalN.Inverse().Transpose(); + + // Allocate final mesh data. + // We'll allocate one mesh for each material. (we'll strip unused ones after) + std::vector meshes(sib->mtls.size()); + + // Un-index the source data and apply to each vertex. + for (unsigned fi=0;fi= meshes.size()) + { + DefaultLogger::get()->error("SIB: Face material index is invalid."); + mtl = 0; + } + + TempMesh& dest = meshes[mtl]; + + aiFace face; + face.mNumIndices = *idx++; + face.mIndices = new unsigned[face.mNumIndices]; + for (unsigned pt=0;ptmeshes.size(); + + // Now that we know the size of everything, + // we can build the final one-material-per-mesh data. + for (size_t n=0;nmName = name; + mesh->mNumFaces = src.faces.size(); + mesh->mFaces = new aiFace[mesh->mNumFaces]; + mesh->mNumVertices = src.vtx.size(); + mesh->mVertices = new aiVector3D[mesh->mNumVertices]; + mesh->mNormals = new aiVector3D[mesh->mNumVertices]; + mesh->mTextureCoords[0] = new aiVector3D[mesh->mNumVertices]; + mesh->mNumUVComponents[0] = 2; + mesh->mMaterialIndex = n; + + for (unsigned i=0;imNumVertices;i++) + { + mesh->mVertices[i] = src.vtx[i]; + mesh->mNormals[i] = src.nrm[i]; + mesh->mTextureCoords[0][i] = src.uv[i]; + } + for (unsigned i=0;imNumFaces;i++) + { + mesh->mFaces[i] = src.faces[i]; + } + + sib->meshes.push_back(mesh); + } + + obj.meshCount = sib->meshes.size() - obj.meshIdx; + sib->objs.push_back(obj); +} + +// ------------------------------------------------------------------------------------------------ +static void ReadMaterial(SIB* sib, StreamReaderLE* stream) +{ + aiColor3D diff = ReadColor(stream); + aiColor3D ambi = ReadColor(stream); + aiColor3D spec = ReadColor(stream); + aiColor3D emis = ReadColor(stream); + float shiny = (float)stream->GetU4(); + + uint32_t nameLen = stream->GetU4(); + aiString name = ReadString(stream, nameLen/2); + uint32_t texLen = stream->GetU4(); + aiString tex = ReadString(stream, texLen/2); + + aiMaterial* mtl = new aiMaterial(); + mtl->AddProperty(&diff, 1, AI_MATKEY_COLOR_DIFFUSE); + mtl->AddProperty(&ambi, 1, AI_MATKEY_COLOR_AMBIENT); + mtl->AddProperty(&spec, 1, AI_MATKEY_COLOR_SPECULAR); + mtl->AddProperty(&emis, 1, AI_MATKEY_COLOR_EMISSIVE); + mtl->AddProperty(&shiny, 1, AI_MATKEY_SHININESS); + mtl->AddProperty(&name, AI_MATKEY_NAME); + if (tex.length > 0) { + mtl->AddProperty(&tex, AI_MATKEY_TEXTURE_DIFFUSE(0)); + mtl->AddProperty(&tex, AI_MATKEY_TEXTURE_AMBIENT(0)); + } + + sib->mtls.push_back(mtl); +} + +// ------------------------------------------------------------------------------------------------ +static void ReadLightInfo(aiLight* light, StreamReaderLE* stream) +{ + uint32_t type = stream->GetU4(); + switch (type) { + case 0: light->mType = aiLightSource_POINT; break; + case 1: light->mType = aiLightSource_SPOT; break; + case 2: light->mType = aiLightSource_DIRECTIONAL; break; + default: light->mType = aiLightSource_UNDEFINED; break; + } + + light->mPosition.x = stream->GetF4(); + light->mPosition.y = stream->GetF4(); + light->mPosition.z = stream->GetF4(); + light->mDirection.x = stream->GetF4(); + light->mDirection.y = stream->GetF4(); + light->mDirection.z = stream->GetF4(); + light->mColorDiffuse = ReadColor(stream); + light->mColorAmbient = ReadColor(stream); + light->mColorSpecular = ReadColor(stream); + float spotExponent = stream->GetF4(); + float spotCutoff = stream->GetF4(); + light->mAttenuationConstant = stream->GetF4(); + light->mAttenuationLinear = stream->GetF4(); + light->mAttenuationQuadratic = stream->GetF4(); + + // Silo uses the OpenGL default lighting model for it's + // spot cutoff/exponent. AssImp unfortunately, does not. + // Let's try and approximate it by solving for the + // 99% and 1% percentiles. + // OpenGL: I = cos(angle)^E + // Solving: angle = acos(I^(1/E)) + float E = 1.0f / std::max(spotExponent, 0.00001f); + float inner = acosf(powf(0.99f, E)); + float outer = acosf(powf(0.01f, E)); + + // Apply the cutoff. + outer = std::min(outer, AI_DEG_TO_RAD(spotCutoff)); + + light->mAngleInnerCone = std::min(inner, outer); + light->mAngleOuterCone = outer; +} + +static void ReadLight(SIB* sib, StreamReaderLE* stream) +{ + aiLight* light = new aiLight(); + + while (stream->GetRemainingSizeToLimit() >= sizeof(SIBChunk)) + { + SIBChunk chunk = ReadChunk(stream); + unsigned oldLimit = stream->SetReadLimit(stream->GetCurrentPos() + chunk.Size); + + switch (chunk.Tag) + { + case TAG('L','N','F','O'): ReadLightInfo(light, stream); break; + case TAG('S','N','A','M'): light->mName = ReadString(stream, chunk.Size/2); break; + default: UnknownChunk(stream, chunk); break; + } + + stream->SetCurrentPos(stream->GetReadLimit()); + stream->SetReadLimit(oldLimit); + } + + sib->lights.push_back(light); +} + +// ------------------------------------------------------------------------------------------------ +static void ReadScale(aiMatrix4x4& axis, StreamReaderLE* stream) +{ + aiMatrix4x4 scale; + scale.a1 = stream->GetF4(); + scale.b1 = stream->GetF4(); + scale.c1 = stream->GetF4(); + scale.d1 = stream->GetF4(); + scale.a2 = stream->GetF4(); + scale.b2 = stream->GetF4(); + scale.c2 = stream->GetF4(); + scale.d2 = stream->GetF4(); + scale.a3 = stream->GetF4(); + scale.b3 = stream->GetF4(); + scale.c3 = stream->GetF4(); + scale.d3 = stream->GetF4(); + scale.a4 = stream->GetF4(); + scale.b4 = stream->GetF4(); + scale.c4 = stream->GetF4(); + scale.d4 = stream->GetF4(); + + axis = axis * scale; +} + +static void ReadInstance(SIB* sib, StreamReaderLE* stream) +{ + SIBObject inst; + uint32_t shapeIndex = 0; + + while (stream->GetRemainingSizeToLimit() >= sizeof(SIBChunk)) + { + SIBChunk chunk = ReadChunk(stream); + unsigned oldLimit = stream->SetReadLimit(stream->GetCurrentPos() + chunk.Size); + + switch (chunk.Tag) + { + case TAG('D','I','N','F'): break; // display info, not needed + case TAG('P','I','N','F'): break; // ? + case TAG('A','X','I','S'): ReadAxis(inst.axis, stream); break; + case TAG('I','N','S','I'): shapeIndex = stream->GetU4(); break; + case TAG('S','M','T','X'): ReadScale(inst.axis, stream); break; + case TAG('S','N','A','M'): inst.name = ReadString(stream, chunk.Size/2); break; + default: UnknownChunk(stream, chunk); break; + } + + stream->SetCurrentPos(stream->GetReadLimit()); + stream->SetReadLimit(oldLimit); + } + + if (shapeIndex >= sib->objs.size()) + throw DeadlyImportError("SIB: Invalid shape index."); + + const SIBObject& src = sib->objs[shapeIndex]; + inst.meshIdx = src.meshIdx; + inst.meshCount = src.meshCount; + sib->insts.push_back(inst); +} + +// ------------------------------------------------------------------------------------------------ +static void CheckVersion(StreamReaderLE* stream) +{ + uint32_t version = stream->GetU4(); + if (version != 1) + throw DeadlyImportError("SIB: Unsupported file version."); +} + +static void ReadScene(SIB* sib, StreamReaderLE* stream) +{ + // Parse each chunk in turn. + while (stream->GetRemainingSizeToLimit() >= sizeof(SIBChunk)) + { + SIBChunk chunk = ReadChunk(stream); + unsigned oldLimit = stream->SetReadLimit(stream->GetCurrentPos() + chunk.Size); + + switch (chunk.Tag) + { + case TAG('H','E','A','D'): CheckVersion(stream); break; + case TAG('S','H','A','P'): ReadShape(sib, stream); break; + case TAG('G','R','P','S'): break; // group assignment, we don't import this + case TAG('T','E','X','P'): break; // ? + case TAG('I','N','S','T'): ReadInstance(sib, stream); break; + case TAG('M','A','T','R'): ReadMaterial(sib, stream); break; + case TAG('L','G','H','T'): ReadLight(sib, stream); break; + default: UnknownChunk(stream, chunk); break; + } + + stream->SetCurrentPos(stream->GetReadLimit()); + stream->SetReadLimit(oldLimit); + } +} + +// ------------------------------------------------------------------------------------------------ +// Imports the given file into the given scene structure. +void SIBImporter::InternReadFile(const std::string& pFile, + aiScene* pScene, IOSystem* pIOHandler) +{ + StreamReaderLE stream(pIOHandler->Open(pFile, "rb")); + + // We should have at least one chunk + if (stream.GetRemainingSize() < 16) + throw DeadlyImportError("SIB file is either empty or corrupt: " + pFile); + + SIB sib; + + // Default material. + aiMaterial* defmtl = new aiMaterial; + aiString defname = aiString(AI_DEFAULT_MATERIAL_NAME); + defmtl->AddProperty(&defname, AI_MATKEY_NAME); + sib.mtls.push_back(defmtl); + + // Read it all. + ReadScene(&sib, &stream); + + // Join the instances and objects together. + size_t firstInst = sib.objs.size(); + sib.objs.insert(sib.objs.end(), sib.insts.begin(), sib.insts.end()); + sib.insts.clear(); + + // Transfer to the aiScene. + pScene->mNumMaterials = sib.mtls.size(); + pScene->mNumMeshes = sib.meshes.size(); + pScene->mNumLights = sib.lights.size(); + pScene->mMaterials = new aiMaterial* [pScene->mNumMaterials]; + pScene->mMeshes = new aiMesh* [pScene->mNumMeshes]; + pScene->mLights = new aiLight* [pScene->mNumLights]; + if (pScene->mNumMaterials) + memcpy(pScene->mMaterials, &sib.mtls[0], sizeof(aiMaterial*) * pScene->mNumMaterials); + if (pScene->mNumMeshes) + memcpy(pScene->mMeshes, &sib.meshes[0], sizeof(aiMesh*) * pScene->mNumMeshes); + if (pScene->mNumLights) + memcpy(pScene->mLights, &sib.lights[0], sizeof(aiLight*) * pScene->mNumLights); + + // Construct the root node. + size_t childIdx = 0; + aiNode *root = new aiNode(); + root->mName.Set(""); + root->mNumChildren = sib.objs.size() + sib.lights.size(); + root->mChildren = new aiNode* [root->mNumChildren]; + pScene->mRootNode = root; + + // Add nodes for each object. + for (size_t n=0;nmChildren[childIdx++] = node; + node->mName = obj.name; + node->mParent = root; + node->mTransformation = obj.axis; + + node->mNumMeshes = obj.meshCount; + node->mMeshes = new unsigned[node->mNumMeshes]; + for (unsigned i=0;imNumMeshes;i++) + node->mMeshes[i] = obj.meshIdx + i; + + // Mark instanced objects as being so. + if (n >= firstInst) + { + node->mMetaData = new aiMetadata; + node->mMetaData->mNumProperties = 1; + node->mMetaData->mKeys = new aiString[1]; + node->mMetaData->mValues = new aiMetadataEntry[1]; + node->mMetaData->Set(0, "IsInstance", true); + } + } + + // Add nodes for each light. + // (no transformation as the light is already in world space) + for (size_t n=0;nmChildren[childIdx++] = node; + node->mName = light->mName; + node->mParent = root; + } +} + +#endif // !! ASSIMP_BUILD_NO_SIB_IMPORTER diff --git a/code/SIBImporter.h b/code/SIBImporter.h new file mode 100644 index 000000000..b68972a81 --- /dev/null +++ b/code/SIBImporter.h @@ -0,0 +1,119 @@ +/* +Open Asset Import Library (assimp) +---------------------------------------------------------------------- + +Copyright (c) 2006-2015, 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 SIBImporter.h + * @brief Declaration of the SIB importer class. + */ +#ifndef AI_SIBIMPORTER_H_INCLUDED +#define AI_SIBIMPORTER_H_INCLUDED + +#include "BaseImporter.h" +#include "../include/assimp/types.h" +#include + +namespace Assimp { + +// --------------------------------------------------------------------------- +/** Importer class for the Nevercenter Silo SIB scene format +*/ +class SIBImporter : public BaseImporter +{ +public: + SIBImporter(); + ~SIBImporter(); + + +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; + +protected: + + // ------------------------------------------------------------------- + /** Return importer meta information. + * See #BaseImporter::GetInfo for the details + */ + const aiImporterDesc* GetInfo () const; + + // ------------------------------------------------------------------- + /** Imports the given file into the given scene structure. + * See BaseImporter::InternReadFile() for details + */ + void InternReadFile( const std::string& pFile, aiScene* pScene, + IOSystem* pIOHandler); + +private: + + struct MeshInformation + { + explicit MeshInformation(const std::string& _name) + : name(_name) + { + vertices.reserve(100); + colors.reserve(100); + } + + std::string name; + + std::vector vertices; + std::vector colors; + }; + + struct GroupInformation + { + explicit GroupInformation(const std::string& _name) + : name(_name) + { + meshes.reserve(10); + } + + std::string name; + std::vector meshes; + }; +}; + +} // end of namespace Assimp + +#endif // AI_SIBIMPORTER_H_INCLUDED diff --git a/code/StreamReader.h b/code/StreamReader.h index 0698d99f9..1f858b691 100644 --- a/code/StreamReader.h +++ b/code/StreamReader.h @@ -253,24 +253,26 @@ public: * * @param limit Maximum number of bytes to be read from * the beginning of the file. Specifying UINT_MAX - * resets the limit to the original end of the stream. */ - void SetReadLimit(unsigned int _limit) { - + * resets the limit to the original end of the stream. + * Returns the previously set limit. */ + unsigned int SetReadLimit(unsigned int _limit) { + unsigned int prev = GetReadLimit(); if (UINT_MAX == _limit) { limit = end; - return; + return prev; } limit = buffer + _limit; if (limit > end) { throw DeadlyImportError("StreamReader: Invalid read limit"); } + return prev; } // --------------------------------------------------------------------- /** Get the current read limit in bytes. Reading over this limit * accidentially raises an exception. */ - int GetReadLimit() const { + unsigned int GetReadLimit() const { return (unsigned int)(limit - buffer); } diff --git a/test/models/SIB/This Way Up.png b/test/models/SIB/This Way Up.png new file mode 100644 index 0000000000000000000000000000000000000000..87c8ba1dcf9de4334ed9879da41a776b6c44c22b GIT binary patch literal 5922 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 Avj6}9 literal 0 HcmV?d00001 diff --git a/test/models/SIB/UV Mapping.png b/test/models/SIB/UV Mapping.png new file mode 100644 index 0000000000000000000000000000000000000000..3d8e235d463c8adc3360e6e2487e83a83c71d016 GIT binary patch literal 7069 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 GiT-~dcx+t& literal 0 HcmV?d00001 diff --git a/test/models/SIB/heffalump.sib b/test/models/SIB/heffalump.sib new file mode 100644 index 0000000000000000000000000000000000000000..00d6bfb51e9b6e4b1fdd21455ad5b83bb3d9f83a GIT binary patch literal 55682 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 MJ-_GgssFa_Z)e$xApigX literal 0 HcmV?d00001 diff --git a/test/models/SIB/readme.txt b/test/models/SIB/readme.txt new file mode 100644 index 000000000..4a83c0ea4 --- /dev/null +++ b/test/models/SIB/readme.txt @@ -0,0 +1,3 @@ +Made by me (Richard Mitton). +I waive all rights to this and release it into the public domain. +Do with it as you wish.