#include "QD3D.h" #include "PommeDebug.h" #include "Pomme.h" #include "3DMFInternal.h" #if !(POMME_DEBUG_3DMF) #define printf(...) do{}while(0) #endif class Q3MetaFile_EarlyEOFException : public std::exception { public: const char *what() const noexcept override { return "Early EOF in 3DMF"; } }; static void Assert(bool condition, const char* message) { if (!condition) { throw std::runtime_error(message); } } template static void ReadTriangleVertexIndices(Pomme::BigEndianIStream& f, int numTriangles, TQ3TriMeshData* currentMesh) { for (int i = 0; i < numTriangles; i++) { T v0 = f.Read(); T v1 = f.Read(); T v2 = f.Read(); currentMesh->triangles[i] = {v0, v1, v2}; } } Q3MetaFileParser::Q3MetaFileParser(std::istream& theBaseStream, TQ3MetaFile& dest) : metaFile(dest) , baseStream(theBaseStream) , f(baseStream) , currentMesh(nullptr) { } uint32_t Q3MetaFileParser::Parse1Chunk() { Assert(currentDepth >= 0, "depth underflow"); //----------------------------------------------------------- // Get current chunk offset, type, size uint32_t chunkOffset = f.Tell(); uint32_t chunkType = f.Read(); uint32_t chunkSize = f.Read(); // std::string myChunk = Pomme::FourCCString(chunkType); // const char* myChunkC = myChunk.c_str(); printf("\n%d-%08x ", currentDepth, chunkOffset); for (int i = 0; i < 1 + currentDepth - (chunkType=='endg'?1:0); i++) printf("\t"); printf("%s\t", Pomme::FourCCString(chunkType).c_str()); fflush(stdout); //----------------------------------------------------------- // Process current chunk switch (chunkType) { case 0: // Happens in Diloph_Fin.3df at 0x00014233 -- signals early EOF? or corrupted file? either way, stop parsing. throw Q3MetaFile_EarlyEOFException(); case 'cntr': // Container { if (currentDepth == 1) __Q3EnlargeArray(metaFile.topLevelGroups, metaFile.numTopLevelGroups, 'GLST'); currentDepth++; auto limit = f.Tell() + (std::streamoff) chunkSize; while (f.Tell() != limit) Parse1Chunk(); currentDepth--; currentMesh = nullptr; break; } case 'bgng': if (currentDepth == 1) __Q3EnlargeArray(metaFile.topLevelGroups, metaFile.numTopLevelGroups, 'GLST'); currentDepth++; f.Skip(chunkSize); // bgng itself typically contains dspg, dgst while ('endg' != Parse1Chunk()) ; currentDepth--; currentMesh = nullptr; break; case 'endg': Assert(chunkSize == 0, "illegal endg size"); break; case 'tmsh': // TriMesh { Assert(!currentMesh, "nested meshes not supported"); Parse_tmsh(chunkSize); Assert(currentMesh, "currentMesh wasn't get set by Parse_tmsh?"); if (metaFile.numTopLevelGroups == 0) __Q3EnlargeArray(metaFile.topLevelGroups, metaFile.numTopLevelGroups, 'GLST'); TQ3TriMeshFlatGroup* group = &metaFile.topLevelGroups[metaFile.numTopLevelGroups-1]; __Q3EnlargeArray(group->meshes, group->numMeshes, 'GMSH'); group->meshes[group->numMeshes-1] = currentMesh; break; } case 'atar': // AttributeArray Parse_atar(chunkSize); break; case 'attr': // AttributeSet Assert(chunkSize == 0, "illegal attr size"); break; case 'kdif': // Diffuse Color Assert(chunkSize == 12, "illegal kdif size"); Assert(currentMesh, "stray kdif"); { static_assert(sizeof(float) == 4); currentMesh->diffuseColor.r = f.Read(); currentMesh->diffuseColor.g = f.Read(); currentMesh->diffuseColor.b = f.Read(); } break; case 'kxpr': // Transparency Color Assert(chunkSize == 12, "illegal kxpr size"); Assert(currentMesh, "stray kxpr"); { static_assert(sizeof(float) == 4); float r = f.Read(); float g = f.Read(); float b = f.Read(); float a = r; printf("%.3f %.3f %.3f\t", r, g, b); Assert(r == g && g == b, "kxpr: expecting all components to be equal"); currentMesh->diffuseColor.a = a; } break; case 'txsu': // TextureShader { uint32_t internalTextureID; Assert(chunkSize == 0, "illegal txsu size"); if (knownTextures.find(chunkOffset) != knownTextures.end()) { // We've seen this 'txsu' before. We're here because a 'rfrn' refers to it again. // Don't create a new texture for it. printf("Already seen this txsu."); internalTextureID = knownTextures[chunkOffset]; // TODO: Just skip to end of container } else { internalTextureID = metaFile.numTextures; __Q3EnlargeArray(metaFile.textures, metaFile.numTextures, 'TXSU'); knownTextures[chunkOffset] = internalTextureID; } if (currentMesh) { Assert(currentMesh->internalTextureID < 0, "txmm: current mesh already has a texture"); Assert(currentMesh->texturingMode == kQ3TexturingModeOff, "txmm: current mesh already has a texturing mode"); currentMesh->internalTextureID = internalTextureID; currentMesh->texturingMode = kQ3TexturingModeInvalid; // set texturing mode to invalid because we don't know if the texture is opaque yet } break; } case 'txmm': // MipmapTexture (after a txsu) case 'txpm': // PixmapTexture (after a txsu) if (GetCurrentTextureShader().pixmap) { printf("Pixmap already set for this txsu\n"); f.Skip(chunkSize); } else { GetCurrentTextureShader().pixmap = ParsePixmap(chunkType, chunkSize); } break; case 'shdr': // UV clamp/wrap (after a txsu) Assert(chunkSize == 8, "illegal shdr size"); GetCurrentTextureShader().boundaryU = (TQ3ShaderUVBoundary) f.Read(); GetCurrentTextureShader().boundaryV = (TQ3ShaderUVBoundary) f.Read(); break; case 'rfrn': // Reference (into TOC) { Assert(chunkSize == 4, "illegal rfrn size"); uint32_t target = f.Read(); printf("TOC#%d -----> %08lx", target, referenceTOC.at(target).offset); auto jumpBackTo = f.Tell(); f.Goto(referenceTOC.at(target).offset); Parse1Chunk(); f.Goto(jumpBackTo); break; } case 'toc ': // Already read TOC at beginning f.Skip(chunkSize); break; default: throw std::runtime_error("unrecognized 3DMF chunk"); } return chunkType; } void Q3MetaFileParser::Parse3DMF() { baseStream.seekg(0, std::ios::end); std::streampos fileLength = baseStream.tellg(); baseStream.seekg(0, std::ios::beg); Assert(f.Read() == '3DMF', "Not a 3DMF file"); Assert(f.Read() == 16, "Bad header length"); uint16_t versionMajor = f.Read(); uint16_t versionMinor = f.Read(); Assert(versionMajor == 1 && (versionMinor == 5 || versionMinor == 6), "Unsupported 3DMF version"); uint32_t flags = f.Read(); Assert(flags == 0, "Database or Stream aren't supported"); uint64_t tocOffset = f.Read(); printf("Version %d.%d tocOffset %08lx\n", versionMajor, versionMinor, tocOffset); // Read TOC if (tocOffset != 0) { Pomme::StreamPosGuard rewindAfterTOC(baseStream); f.Goto(tocOffset); Assert('toc ' == f.Read(), "Expecting toc magic here"); f.Skip(4); //uint32_t tocSize = f.Read(); f.Skip(8); //uint64_t nextToc = f.Read(); f.Skip(4); //uint32_t refSeed = f.Read(); f.Skip(4); //uint32_t typeSeed = f.Read(); uint32_t tocEntryType = f.Read(); uint32_t tocEntrySize = f.Read(); uint32_t nEntries = f.Read(); Assert(tocEntryType == 1, "only QD3D 1.5 3DMF TOCs are recognized"); Assert(tocEntrySize == 16, "incorrect tocEntrySize"); for (uint32_t i = 0; i < nEntries;i++) { uint32_t refID = f.Read(); uint64_t objLocation = f.Read(); uint32_t objType = f.Read(); printf("TOC: refID %d '%s' at %08lx\n", refID, Pomme::FourCCString(objType).c_str(), objLocation); referenceTOC[refID] = {objLocation, objType}; } } // Chunk Loop try { while (f.Tell() != fileLength) { Parse1Chunk(); } } catch (Q3MetaFile_EarlyEOFException&) { // Stop parsing printf("Early EOF"); } printf("\n"); } void Q3MetaFileParser::Parse_tmsh(uint32_t chunkSize) { Assert(chunkSize >= 52, "Illegal tmsh size"); Assert(!currentMesh, "current mesh already set"); uint32_t numTriangles = f.Read(); f.Skip(4); // numTriangleAttributes (u32) -- don't care how many attribs there are, we'll read them in as we go uint32_t numEdges = f.Read(); uint32_t numEdgeAttributes = f.Read(); uint32_t numVertices = f.Read(); f.Skip(4); // numVertexAttributes (u32) -- don't care how many attribs there are, we'll read them in as we go printf("%d tris, %d vertices\t", numTriangles, numVertices); Assert(0 == numEdges, "edges are not supported"); Assert(0 == numEdgeAttributes, "edge attributes are not supported"); // Allocate the mesh. // Don't allocate vertex UVs, colors or normals yet. We'll allocate them later if the mesh needs them. currentMesh = Q3TriMeshData_New(numTriangles, numVertices, 0); __Q3EnlargeArray(metaFile.meshes, metaFile.numMeshes, 'MLST'); metaFile.meshes[metaFile.numMeshes-1] = currentMesh; // Triangles if (numVertices <= 0xFF) { ReadTriangleVertexIndices(f, numTriangles, currentMesh); } else if (numVertices <= 0xFFFF) { ReadTriangleVertexIndices(f, numTriangles, currentMesh); } else { ReadTriangleVertexIndices(f, numTriangles, currentMesh); } // Ensure all vertex indices are in the expected range for (uint32_t i = 0; i < numTriangles; i++) { for (auto index : currentMesh->triangles[i].pointIndices) { Assert(index < numVertices, "3DMF parser: vertex index out of range"); } } // Edges // (not supported yet) // Vertices for (uint32_t i = 0; i < numVertices; i++) { float x = f.Read(); float y = f.Read(); float z = f.Read(); //printf("%f %f %f\n", vertexX, vertexY, vertexZ); currentMesh->points[i] = {x, y, z}; } // Bounding box { float xMin = f.Read(); float yMin = f.Read(); float zMin = f.Read(); float xMax = f.Read(); float yMax = f.Read(); float zMax = f.Read(); uint32_t emptyFlag = f.Read(); currentMesh->bBox.min = {xMin, yMin, zMin}; currentMesh->bBox.max = {xMax, yMax, zMax}; currentMesh->bBox.isEmpty = emptyFlag? kQ3True: kQ3False; //printf("%f %f %f - %f %f %f (empty? %d)\n", xMin, yMin, zMin, xMax, yMax, zMax, emptyFlag); } } void Q3MetaFileParser::Parse_atar(uint32_t chunkSize) { Assert(chunkSize >= 20, "Illegal atar size"); Assert(currentMesh, "no current mesh"); uint32_t attributeType = f.Read(); Assert(0 == f.Read(), "expected zero here"); uint32_t positionOfArray = f.Read(); uint32_t positionInArray = f.Read(); // what's that? uint32_t attributeUseFlag = f.Read(); Assert(attributeType >= 1 && attributeType < kQ3AttributeTypeNumTypes, "illegal attribute type"); Assert(positionOfArray <= 2, "illegal position of array"); Assert(attributeUseFlag <= 1, "unrecognized attribute use flag"); bool isTriangleAttribute = positionOfArray == 0; bool isVertexAttribute = positionOfArray == 2; Assert(isTriangleAttribute || isVertexAttribute, "only face or vertex attributes are supported"); if (isVertexAttribute && (attributeType == kQ3AttributeTypeShadingUV || attributeType == kQ3AttributeTypeSurfaceUV)) { printf("vertex UVs"); Assert(!currentMesh->vertexUVs, "current mesh already had a vertex UV array"); currentMesh->vertexUVs = __Q3Alloc(currentMesh->numPoints, 'TMuv'); for (int i = 0; i < currentMesh->numPoints; i++) { float u = f.Read(); float v = f.Read(); currentMesh->vertexUVs[i] = {u, 1-v}; } } else if (isVertexAttribute && attributeType == kQ3AttributeTypeNormal) { printf("vertex normals"); Assert(positionInArray == 0, "PIA must be 0 for normals"); Assert(!currentMesh->vertexNormals, "current mesh already had a vertex normal array"); currentMesh->vertexNormals = __Q3Alloc(currentMesh->numPoints, 'TMvn'); currentMesh->hasVertexNormals = true; for (int i = 0; i < currentMesh->numPoints; i++) { currentMesh->vertexNormals[i].x = f.Read(); currentMesh->vertexNormals[i].y = f.Read(); currentMesh->vertexNormals[i].z = f.Read(); } } else if (isVertexAttribute && attributeType == kQ3AttributeTypeDiffuseColor) // used in Bugdom's Global_Models2.3dmf { printf("vertex diffuse"); // Assert(positionInArray == 0, "PIA must be 0 for colors"); Assert(!currentMesh->vertexColors, "current mesh already had a vertex color array"); currentMesh->vertexColors = __Q3Alloc(currentMesh->numPoints, 'TMvc'); currentMesh->hasVertexColors = true; for (int i = 0; i < currentMesh->numPoints; i++) { currentMesh->vertexColors[i].r = f.Read(); currentMesh->vertexColors[i].g = f.Read(); currentMesh->vertexColors[i].b = f.Read(); currentMesh->vertexColors[i].a = 1.0f; } } else if (isTriangleAttribute && attributeType == kQ3AttributeTypeNormal) // face normals { printf("face normals (ignore)"); f.Skip(currentMesh->numTriangles * 3 * 4); } else { Assert(false, "unsupported combo"); } } TQ3Pixmap* Q3MetaFileParser::ParsePixmap(uint32_t chunkType, uint32_t chunkSize) { size_t chunkHeaderSize = chunkType == 'txmm'? 8*4: 7*4; Assert(chunkSize >= chunkHeaderSize, "incorrect chunk header size"); uint32_t pixelType; uint32_t bitOrder; uint32_t byteOrder; uint32_t width; uint32_t height; uint32_t rowBytes; if (chunkType == 'txmm') { uint32_t useMipmapping = f.Read(); pixelType = f.Read(); bitOrder = f.Read(); byteOrder = f.Read(); width = f.Read(); height = f.Read(); rowBytes = f.Read(); uint32_t offset = f.Read(); Assert(!useMipmapping, "mipmapping not supported"); Assert(offset == 0, "unsupported texture offset"); } else if (chunkType == 'txpm') { width = f.Read(); height = f.Read(); rowBytes = f.Read(); f.Skip(4); //pixelSize = f.Read(); pixelType = f.Read(); bitOrder = f.Read(); byteOrder = f.Read(); } else { Assert(false, "ParsePixmap: Illegal chunkType"); return nullptr; } uint32_t imageSize = rowBytes * height; if ((imageSize & 3) != 0) imageSize = (imageSize & 0xFFFFFFFC) + 4; Assert(chunkSize == chunkHeaderSize + imageSize, "incorrect chunk size"); Assert(bitOrder == kQ3EndianBig, "unsupported bit order"); #if POMME_DEBUG_3DMF printf("%d*%d rb=%d", width, height, rowBytes); switch (pixelType) { case kQ3PixelTypeRGB32: printf(" RGB32"); break; case kQ3PixelTypeARGB32: printf(" ARGB32"); break; case kQ3PixelTypeRGB16: printf(" RGB16"); break; case kQ3PixelTypeARGB16: printf(" ARGB16"); break; case kQ3PixelTypeRGB16_565: printf(" RGB16_565"); break; case kQ3PixelTypeRGB24: printf(" RGB24"); break; case kQ3PixelTypeRGBA32: printf(" RGBA32"); break; default: printf(" UnknownPixelType"); break; } #endif // Find bytes per pixel int bytesPerPixel = 0; if (pixelType == kQ3PixelTypeRGB16 || pixelType == kQ3PixelTypeARGB16) bytesPerPixel = 2; else if (pixelType == kQ3PixelTypeRGB32 || pixelType == kQ3PixelTypeARGB32) bytesPerPixel = 4; else Assert(false, "unrecognized pixel type"); int trimmedRowBytes = bytesPerPixel * width; TQ3Pixmap* pixmap = __Q3Alloc(1, 'PXMP'); pixmap->pixelType = pixelType; pixmap->bitOrder = bitOrder; pixmap->byteOrder = byteOrder; pixmap->width = width; pixmap->height = height; pixmap->pixelSize = bytesPerPixel * 8; pixmap->rowBytes = trimmedRowBytes; pixmap->image = __Q3Alloc(trimmedRowBytes * height, 'IMAG'); // Trim padding at end of rows for (uint32_t y = 0; y < height; y++) { f.Read((Ptr) pixmap->image + y*pixmap->rowBytes, pixmap->rowBytes); f.Skip(rowBytes - width * bytesPerPixel); } // Convert to native endianness (especially to avoid breaking 16-bit 1-5-5-5 ARGB textures) if (byteOrder != kQ3EndianNative) { ByteswapInts(bytesPerPixel, width*height, pixmap->image); pixmap->byteOrder = kQ3EndianNative; } Q3Pixmap_ApplyEdgePadding(pixmap); return pixmap; } TQ3TextureShader& Q3MetaFileParser::GetCurrentTextureShader() { Assert(metaFile.numTextures > 0, "txmm/txpm: no txsu opened"); return metaFile.textures[metaFile.numTextures - 1]; }