// 3dsread.cpp // // Copyright (C) 2000, Chris Laurel // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License // as published by the Free Software Foundation; either version 2 // of the License, or (at your option) any later version. #include #include #include #include #include #include #include #include #include #include #include "3dsmodel.h" #include "3dsread.h" namespace celutil = celestia::util; using celestia::util::GetLogger; namespace { constexpr std::int32_t READ_FAILURE = -1; constexpr std::int32_t UNKNOWN_CHUNK = -2; enum class M3DChunkType : std::uint16_t { Null = 0x0000, Version = 0x0002, ColorFloat = 0x0010, Color24 = 0x0011, LinColorF = 0x0013, IntPercentage = 0x0030, FloatPercentage = 0x0031, MasterScale = 0x0100, BackgroundColor = 0x1200, Meshdata = 0x3d3d, MeshVersion = 0x3d3e, NamedObject = 0x4000, TriangleMesh = 0x4100, PointArray = 0x4110, PointFlagArray = 0x4111, FaceArray = 0x4120, MeshMaterialGroup = 0x4130, MeshTextureCoords = 0x4140, MeshSmoothGroup = 0x4150, MeshMatrix = 0x4160, Magic = 0x4d4d, MaterialName = 0xa000, MaterialAmbient = 0xa010, MaterialDiffuse = 0xa020, MaterialSpecular = 0xa030, MaterialShininess = 0xa040, MaterialShin2Pct = 0xa041, MaterialTransparency = 0xa050, MaterialXpfall = 0xa052, MaterialRefblur = 0xa053, MaterialSelfIllum = 0xa084, MaterialWiresize = 0xa087, MaterialXpfallin = 0xa08a, MaterialShading = 0xa100, MaterialTexmap = 0xa200, MaterialMapname = 0xa300, MaterialEntry = 0xafff, Kfdata = 0xb000, }; template using ProcessChunkFunc = std::int32_t (*)(std::istream&, M3DChunkType, std::int32_t, T&); std::int32_t readString(std::istream& in, std::string& value) { constexpr std::size_t maxLength = 1024; char s[maxLength]; for (std::size_t count = 0; count < maxLength; count++) { in.read(s + count, 1); if (!in.good()) { return READ_FAILURE; } if (s[count] == '\0') { value = s; return count + 1; } } return READ_FAILURE; } bool readChunkType(std::istream& in, M3DChunkType& chunkType) { std::uint16_t value; if (!celutil::readLE(in, value)) { return false; } chunkType = static_cast(value); return true; } template std::int32_t read3DSChunk(std::istream& in, ProcessChunkFunc chunkFunc, T& obj) { M3DChunkType chunkType; if (!readChunkType(in, chunkType)) { return READ_FAILURE; } std::int32_t chunkSize; if (!celutil::readLE(in, chunkSize) || chunkSize < 6) { return READ_FAILURE; } std::int32_t contentSize = chunkSize - 6; std::int32_t processedSize = chunkFunc(in, chunkType, contentSize, obj); switch (processedSize) { case READ_FAILURE: return READ_FAILURE; case UNKNOWN_CHUNK: in.ignore(contentSize); return in.good() ? chunkSize : READ_FAILURE; default: if (processedSize != contentSize) { GetLogger()->error("Chunk type {:04x}, expected {} bytes but read {}\n", static_cast(chunkType), contentSize, processedSize); return READ_FAILURE; } return chunkSize; } } template std::int32_t read3DSChunks(std::istream& in, std::int32_t nBytes, ProcessChunkFunc chunkFunc, T& obj) { std::int32_t bytesRead = 0; while (bytesRead < nBytes) { std::int32_t chunkSize = read3DSChunk(in, chunkFunc, obj); if (chunkSize < 0) { GetLogger()->error("Failed to read 3DS chunk\n"); return READ_FAILURE; } bytesRead += chunkSize; } if (bytesRead != nBytes) { GetLogger()->error("Multiple chunks, expected {} bytes but read {}\n", nBytes, bytesRead); return READ_FAILURE; } return bytesRead; } std::int32_t readColor(std::istream& in, M3DColor& color) { std::uint8_t r, g, b; if (!celutil::readLE(in, r) || !celutil::readLE(in, g) || !celutil::readLE(in, b)) { return READ_FAILURE; } color = {static_cast(r) / 255.0f, static_cast(g) / 255.0f, static_cast(b) / 255.0f}; return 3; } std::int32_t readFloatColor(std::istream& in, M3DColor& color) { float r, g, b; if (!celutil::readLE(in, r) || !celutil::readLE(in, g) || !celutil::readLE(in, b)) { return READ_FAILURE; } color = { r, g, b }; return static_cast(3 * sizeof(float)); } std::int32_t readMeshMatrix(std::istream& in, Eigen::Matrix4f& m) { float elements[12]; for (std::size_t i = 0; i < 12; ++i) { if (!celutil::readLE(in, elements[i])) { return READ_FAILURE; } } m << elements[0], elements[1], elements[2], 0, elements[3], elements[4], elements[5], 0, elements[6], elements[7], elements[8], 0, elements[9], elements[10], elements[11], 1; return static_cast(12 * sizeof(float)); } std::int32_t readPointArray(std::istream& in, M3DTriangleMesh& triMesh) { std::uint16_t nPoints; if (!celutil::readLE(in, nPoints)) { return READ_FAILURE; } std::int32_t bytesRead = static_cast(sizeof(nPoints)); for (int i = 0; i < static_cast(nPoints); i++) { float x, y, z; if (!celutil::readLE(in, x) || !celutil::readLE(in, y) || !celutil::readLE(in, z)) { return READ_FAILURE; } bytesRead += static_cast(3 * sizeof(float)); triMesh.addVertex(Eigen::Vector3f(x, y, z)); } return bytesRead; } std::int32_t readTextureCoordArray(std::istream& in, M3DTriangleMesh& triMesh) { std::int32_t bytesRead = 0; std::uint16_t nPoints; if (!celutil::readLE(in, nPoints)) { return READ_FAILURE; } bytesRead += static_cast(sizeof(nPoints)); for (int i = 0; i < static_cast(nPoints); i++) { float u, v; if (!celutil::readLE(in, u) || !celutil::readLE(in, v)) { return READ_FAILURE; } bytesRead += static_cast(2 * sizeof(float)); triMesh.addTexCoord(Eigen::Vector2f(u, -v)); } return bytesRead; } std::int32_t processFaceArrayChunk(std::istream& in, M3DChunkType chunkType, std::int32_t /*contentSize*/, M3DTriangleMesh& triMesh) { std::int32_t bytesRead = 0; std::uint16_t nFaces; switch (chunkType) { case M3DChunkType::MeshMaterialGroup: { M3DMeshMaterialGroup matGroup; bytesRead = readString(in, matGroup.materialName); if (bytesRead == READ_FAILURE || !celutil::readLE(in, nFaces)) { return READ_FAILURE; } bytesRead += static_cast(sizeof(nFaces)); for (std::uint16_t i = 0; i < nFaces; i++) { std::uint16_t faceIndex; if (!celutil::readLE(in, faceIndex)) { return READ_FAILURE; } bytesRead += static_cast(sizeof(faceIndex)); matGroup.faces.push_back(faceIndex); } triMesh.addMeshMaterialGroup(std::move(matGroup)); return bytesRead; } case M3DChunkType::MeshSmoothGroup: nFaces = triMesh.getFaceCount(); for (std::uint16_t i = 0; i < nFaces; i++) { std::int32_t groups; if (!celutil::readLE(in, groups) || groups < 0){ return READ_FAILURE; } bytesRead += static_cast(sizeof(groups)); triMesh.addSmoothingGroups(static_cast(groups)); } return bytesRead; default: return UNKNOWN_CHUNK; } } std::int32_t readFaceArray(std::istream& in, M3DTriangleMesh& triMesh, std::int32_t contentSize) { std::uint16_t nFaces; if (!celutil::readLE(in, nFaces)) { return READ_FAILURE; } std::int32_t bytesRead = static_cast(sizeof(nFaces)); for (int i = 0; i < static_cast(nFaces); i++) { std::uint16_t v0, v1, v2, flags; if (!celutil::readLE(in, v0) || !celutil::readLE(in, v1) || !celutil::readLE(in, v2) || !celutil::readLE(in, flags)) { return READ_FAILURE; } bytesRead += static_cast(4 * sizeof(std::uint16_t)); triMesh.addFace(v0, v1, v2); } if (bytesRead > contentSize) { return READ_FAILURE; } if (bytesRead < contentSize) { std::int32_t trailingSize = read3DSChunks(in, contentSize - bytesRead, processFaceArrayChunk, triMesh); bytesRead += trailingSize; } return bytesRead; } std::int32_t processTriMeshChunk(std::istream& in, M3DChunkType chunkType, std::int32_t contentSize, M3DTriangleMesh& triMesh) { switch (chunkType) { case M3DChunkType::PointArray: return readPointArray(in, triMesh); case M3DChunkType::MeshTextureCoords: return readTextureCoordArray(in, triMesh); case M3DChunkType::FaceArray: return readFaceArray(in, triMesh, contentSize); case M3DChunkType::MeshMatrix: { Eigen::Matrix4f matrix; std::int32_t bytesRead = readMeshMatrix(in, matrix); if (bytesRead < 0) { return READ_FAILURE; } triMesh.setMatrix(matrix); return bytesRead; } default: return UNKNOWN_CHUNK; } } std::int32_t processModelChunk(std::istream& in, M3DChunkType chunkType, std::int32_t contentSize, M3DModel& model) { if (chunkType == M3DChunkType::TriangleMesh) { M3DTriangleMesh triMesh; std::int32_t bytesRead = read3DSChunks(in, contentSize, processTriMeshChunk, triMesh); if (bytesRead == READ_FAILURE) { return READ_FAILURE; } model.addTriMesh(std::move(triMesh)); return bytesRead; } return UNKNOWN_CHUNK; } std::int32_t processColorChunk(std::istream& in, M3DChunkType chunkType, std::int32_t /*contentSize*/, M3DColor& color) { switch (chunkType) { case M3DChunkType::Color24: return readColor(in, color); case M3DChunkType::ColorFloat: return readFloatColor(in, color); default: return UNKNOWN_CHUNK; } } std::int32_t processPercentageChunk(std::istream& in, M3DChunkType chunkType, std::int32_t /*contentSize*/, float& percent) { switch (chunkType) { case M3DChunkType::IntPercentage: { std::int16_t value; if (!celutil::readLE(in, value)) { return READ_FAILURE; } percent = static_cast(value); return sizeof(value); } case M3DChunkType::FloatPercentage: return celutil::readLE(in, percent) ? sizeof(float) : READ_FAILURE; default: return UNKNOWN_CHUNK; } } std::int32_t processTexmapChunk(std::istream& in, M3DChunkType chunkType, std::int32_t /*contentSize*/, M3DMaterial& material) { if (chunkType == M3DChunkType::MaterialMapname) { std::string name; std::int32_t bytesRead = readString(in, name); if (bytesRead < 0) { return READ_FAILURE; } material.setTextureMap(name); return bytesRead; } return UNKNOWN_CHUNK; } std::int32_t processMaterialChunk(std::istream& in, M3DChunkType chunkType, std::int32_t contentSize, M3DMaterial& material) { std::int32_t bytesRead; std::string name; M3DColor color; float t; switch (chunkType) { case M3DChunkType::MaterialName: bytesRead = readString(in, name); if (bytesRead < 0) { return READ_FAILURE; } material.setName(std::move(name)); return bytesRead; case M3DChunkType::MaterialAmbient: bytesRead = read3DSChunks(in, contentSize, processColorChunk, color); if (bytesRead < 0) { return READ_FAILURE; } material.setAmbientColor(color); return bytesRead; case M3DChunkType::MaterialDiffuse: bytesRead = read3DSChunks(in, contentSize, processColorChunk, color); if (bytesRead < 0) { return READ_FAILURE; } material.setDiffuseColor(color); return bytesRead; case M3DChunkType::MaterialSpecular: bytesRead = read3DSChunks(in, contentSize, processColorChunk, color); if (bytesRead < 0) { return READ_FAILURE; } material.setSpecularColor(color); return bytesRead; case M3DChunkType::MaterialShininess: bytesRead = read3DSChunks(in, contentSize, processPercentageChunk, t); if (bytesRead < 0) { return READ_FAILURE; } material.setShininess(t); return bytesRead; case M3DChunkType::MaterialTransparency: bytesRead = read3DSChunks(in, contentSize, processPercentageChunk, t); if (bytesRead < 0) { return READ_FAILURE; } material.setOpacity(1.0f - t / 100.0f); return bytesRead; case M3DChunkType::MaterialTexmap: return read3DSChunks(in, contentSize, processTexmapChunk, material); default: return UNKNOWN_CHUNK; } } std::int32_t processSceneChunk(std::istream& in, M3DChunkType chunkType, std::int32_t contentSize, M3DScene& scene) { switch (chunkType) { case M3DChunkType::NamedObject: { std::string name; std::int32_t bytesRead = readString(in, name); if (bytesRead < 0) { return READ_FAILURE; } M3DModel model; model.setName(name); std::int32_t chunksSize = read3DSChunks(in, contentSize - bytesRead, processModelChunk, model); if (chunksSize < 0) { return READ_FAILURE; } scene.addModel(std::move(model)); return bytesRead + chunksSize; } case M3DChunkType::MaterialEntry: { M3DMaterial material; std::int32_t bytesRead = read3DSChunks(in, contentSize, processMaterialChunk, material); if (bytesRead < 0) { return READ_FAILURE; } scene.addMaterial(std::move(material)); return bytesRead; } case M3DChunkType::BackgroundColor: { M3DColor color; std::int32_t bytesRead = read3DSChunks(in, contentSize, processColorChunk, color); if (bytesRead < 0) { return READ_FAILURE; } scene.setBackgroundColor(color); return bytesRead; } default: return UNKNOWN_CHUNK; } } std::int32_t processTopLevelChunk(std::istream& in, M3DChunkType chunkType, std::int32_t contentSize, M3DScene& scene) { if (chunkType == M3DChunkType::Meshdata) { return read3DSChunks(in, contentSize, processSceneChunk, scene); } return UNKNOWN_CHUNK; } } // end namespace std::unique_ptr Read3DSFile(std::istream& in) { M3DChunkType chunkType; if (!readChunkType(in, chunkType) || chunkType != M3DChunkType::Magic) { GetLogger()->error("Read3DSFile: Wrong magic number in header\n"); return nullptr; } std::int32_t chunkSize; if (!celutil::readLE(in, chunkSize) || chunkSize < 6) { GetLogger()->error("Read3DSFile: Error reading 3DS file top level chunk size\n"); return nullptr; } GetLogger()->verbose("3DS file, {} bytes\n", chunkSize + 6); auto scene = std::make_unique(); std::int32_t contentSize = chunkSize - 6; std::int32_t bytesRead = read3DSChunks(in, contentSize, processTopLevelChunk, *scene); if (bytesRead < 0) { return nullptr; } if (bytesRead != contentSize) { return nullptr; } return scene; } std::unique_ptr Read3DSFile(const fs::path& filename) { std::ifstream in(filename.string(), std::ios::in | std::ios::binary); if (!in.good()) { GetLogger()->error("Read3DSFile: Error opening {}\n", filename); return nullptr; } std::unique_ptr scene = Read3DSFile(in); in.close(); return scene; } #if 0 int main(int argc, char* argv[]) { if (argc != 2) { cerr << "Usage: 3dsread \n"; exit(1); } ifstream in(argv[1], ios::in | ios::binary); if (!in.good()) { cerr << "Error opening " << argv[1] << '\n'; exit(1); } else { read3DSFile(in); in.close(); } return 0; } #endif