diff --git a/CMakeLists.txt b/CMakeLists.txt index 1fe9427..3e3261b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -51,13 +51,29 @@ set(POMME_SOURCES ${POMME_SRCDIR}/Utilities/StringUtils.h ${POMME_SRCDIR}/Utilities/structpack.cpp ${POMME_SRCDIR}/Utilities/structpack.h - ${POMME_SRCDIR}/Video/Cinepak.cpp - ${POMME_SRCDIR}/Video/Cinepak.h - ${POMME_SRCDIR}/Video/moov.cpp $<$:${POMME_SRCDIR}/Platform/Windows/PommeWindows.cpp> $<$:${POMME_SRCDIR}/Platform/Windows/PommeWindows.h> ) +if (NOT(POMME_NO_VIDEO)) + list(APPEND POMME_SOURCES + ${POMME_SRCDIR}/Video/Cinepak.cpp + ${POMME_SRCDIR}/Video/Cinepak.h + ${POMME_SRCDIR}/Video/moov.cpp + ) +endif() + +if (NOT(POMME_NO_QD3D)) + list(APPEND POMME_SOURCES + ${POMME_SRCDIR}/QD3D/3DMFInternal.h + ${POMME_SRCDIR}/QD3D/3DMFParser.cpp + ${POMME_SRCDIR}/QD3D/QD3D.cpp + ${POMME_SRCDIR}/QD3D/QD3D.h + ${POMME_SRCDIR}/QD3D/QD3DMath.cpp + ${POMME_SRCDIR}/QD3D/QD3DMath.h + ) +endif() + add_library(${PROJECT_NAME} ${POMME_SOURCES}) find_package(SDL2 REQUIRED) @@ -79,10 +95,12 @@ if(MSVC) target_compile_options(${PROJECT_NAME} PRIVATE /EHa /W4 + /wd4068 # ignore unrecognized pragmas /wd4100 # unreferenced formal parameter /wd4201 # nonstandard extension /wd4244 # conversion from double to float /wd4458 # declaration of variable hides class member + /MP ) else() target_compile_options(${PROJECT_NAME} PRIVATE @@ -91,6 +109,7 @@ else() -Wshadow -Wno-multichar -Wno-unused-parameter + -Wno-unknown-pragmas -fexceptions ) endif() diff --git a/LICENSE.md b/LICENSE.md index f22f1eb..182a1dc 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -74,6 +74,45 @@ The following portions are derivate works from -------- +The following portions are derivative works from parts of +[Quesa](https://github.com/jwwalker/Quesa) by the Quesa Developers +(BSD license): +- QD3DMath.h +- QD3DMath.cpp + +> Copyright (c) 1999-2020, Quesa Developers. All rights reserved. +> +> For the current release of Quesa, please see: +> https://github.com/jwwalker/Quesa +> +> Redistribution and use 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 Quesa nor the names of its contributors may be used to +> endorse or promote products derived from this software without specific +> prior written permission. +> +> 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. + +-------- + Portions copied from [gulrak/filesystem](https://github.com/gulrak/filesystem) by Steffen Schümann (MIT license): - filesystem_implementation.hpp diff --git a/src/QD3D/3DMFInternal.h b/src/QD3D/3DMFInternal.h new file mode 100644 index 0000000..dd6c52e --- /dev/null +++ b/src/QD3D/3DMFInternal.h @@ -0,0 +1,157 @@ +#pragma once + +#ifdef __cplusplus + +#include +#include +#include +#include +#include +#include "Utilities/BigEndianIStream.h" + +class Q3MetaFileParser +{ +public: + Q3MetaFileParser(std::istream& input, TQ3MetaFile& dest); + void Parse3DMF(); + +private: + // Returns fourcc of parsed chunk + uint32_t Parse1Chunk(); + + // Parse trimesh chunk + void Parse_tmsh(uint32_t chunkSize); + + // Parse attribute array chunk + void Parse_atar(uint32_t chunkSize); + + // Parse mipmap texture chunk + uint32_t Parse_txmm(uint32_t chunkSize); + + TQ3MetaFile& metaFile; + std::istream& baseStream; + Pomme::BigEndianIStream f; + + int currentDepth = 0; + + TQ3TriMeshData* currentMesh; + + std::map referenceTOC; + std::map knownTextures; +}; + +//----------------------------------------------------------------------------- +// Memory allocator + +#define ALLOCATOR_HEADER_BYTES 16 + +struct __Q3AllocatorCookie +{ + uint32_t classID; + size_t blockSize; // including header cookie +}; + +static_assert(sizeof(__Q3AllocatorCookie) <= ALLOCATOR_HEADER_BYTES); + +template +static T* __Q3Alloc(size_t count, uint32_t classID) +{ + size_t totalBytes = ALLOCATOR_HEADER_BYTES + count*sizeof(T); + + uint8_t* block = (uint8_t*) calloc(totalBytes, 1); + + __Q3AllocatorCookie* cookie = (__Q3AllocatorCookie*) block; + + cookie->classID = classID; + cookie->blockSize = totalBytes; + + return (T*) (block + ALLOCATOR_HEADER_BYTES); +} + +static __Q3AllocatorCookie* __Q3GetCookie(const void* sourcePayload, uint32_t classID) +{ + if (!sourcePayload) + throw std::runtime_error("__Q3GetCookie: got null pointer"); + + uint8_t* block = ((uint8_t*) sourcePayload) - ALLOCATOR_HEADER_BYTES; + + __Q3AllocatorCookie* cookie = (__Q3AllocatorCookie*) block; + + if (classID != cookie->classID) + throw std::runtime_error("__Q3GetCookie: incorrect cookie"); + + return cookie; +} + +static void __Q3Dispose(void* object, uint32_t classID) +{ + if (!object) + return; + + auto cookie = __Q3GetCookie(object, classID); + + memset(cookie, 0, cookie->blockSize); + + cookie->classID = 'DEAD'; + + free(cookie); +} + +template +static T* __Q3Realloc(T* sourcePayload, size_t minCount, uint32_t classID) +{ + if (!sourcePayload) + { + return __Q3Alloc(minCount * 2, classID); + } + + __Q3AllocatorCookie* sourceCookie = __Q3GetCookie(sourcePayload, classID); + + size_t currentBytes = sourceCookie->blockSize - ALLOCATOR_HEADER_BYTES; + size_t wantedBytes = minCount * sizeof(T); + + if (wantedBytes < currentBytes) + { + return sourcePayload; + } + + T* newPayload = __Q3Alloc(minCount * 2, classID); + memcpy(newPayload, sourcePayload, currentBytes); + + __Q3Dispose(sourcePayload, classID); + + return newPayload; +} + +template +static void __Q3EnlargeArray(T*& sourcePayload, int& size, uint32_t classID) +{ + size++; + sourcePayload = __Q3Realloc(sourcePayload, size, classID); +} + +template +static T* __Q3Copy(const T* sourcePayload, uint32_t classID) +{ + if (!sourcePayload) + return nullptr; + + __Q3AllocatorCookie* sourceCookie = __Q3GetCookie(sourcePayload, classID); + + uint8_t* block = (uint8_t*) calloc(sourceCookie->blockSize, 1); + memcpy(block, sourceCookie, sourceCookie->blockSize); + + return (T*) (block + ALLOCATOR_HEADER_BYTES); +} + +template +static void __Q3DisposeArray(T** arrayPtr, uint32_t classID) +{ + if (*arrayPtr) + { + __Q3Dispose(*arrayPtr, classID); + *arrayPtr = nullptr; + } +} + +#endif // __cplusplus diff --git a/src/QD3D/3DMFParser.cpp b/src/QD3D/3DMFParser.cpp new file mode 100644 index 0000000..835d443 --- /dev/null +++ b/src/QD3D/3DMFParser.cpp @@ -0,0 +1,503 @@ +#include "QD3D.h" +#include "PommeDebug.h" +#include "Pomme.h" +#include "3DMFInternal.h" + +// Comment out the line below to get debug output from the 3DMF parser on stdout +#define printf(...) do{}while(0) + +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"); +// Assert(containerEnd == 0, "stray endg"); +// containerEnd = f.Tell(); // force while loop to stop + 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 + Assert(chunkSize == 0, "illegal txsu size"); + break; + + case 'txmm': // MipmapTexture + { + uint32_t internalTextureID; + if (knownTextures.find(chunkOffset) != knownTextures.end()) + { + printf("Texture already seen!"); + internalTextureID = knownTextures[chunkOffset]; + f.Skip(chunkSize); + } + else + { + internalTextureID = Parse_txmm(chunkSize); + knownTextures[chunkOffset] = internalTextureID; + } + + if (currentMesh) + { + Assert(!currentMesh->hasTexture, "txmm: current mesh already has a texture"); + currentMesh->internalTextureID = internalTextureID; + currentMesh->hasTexture = true; + } + + 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)); + auto jumpBackTo = f.Tell(); + f.Goto(referenceTOC.at(target)); + 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(); + + 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; + } + } + + + // 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"); + + currentMesh = Q3TriMeshData_New(numTriangles, numVertices); + + __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 + { + static_assert(sizeof(TQ3TriMeshTriangleData::pointIndices[0]) == 2); + Assert(false, "Meshes exceeding 65535 vertices are not supported"); + //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) + { + printf("vertex UVs"); + Assert(currentMesh->vertexUVs, "current mesh has no vertex UV array"); + 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 has no vertex normal array"); + 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 + { + Assert(false, "per-vertex diffuse color not supported in Nanosaur"); +#if 0 + printf("vertex diffuse"); +// Assert(positionInArray == 0, "PIA must be 0 for colors"); + Assert(currentMesh->vertexNormals, "current mesh has no vertex color array"); + 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(); + } +#endif + } + else if (isTriangleAttribute && attributeType == kQ3AttributeTypeNormal) // face normals + { + printf("face normals (ignore)"); + f.Skip(currentMesh->numTriangles * 3 * 4); + } + else + { + Assert(false, "unsupported combo"); + } +} + +uint32_t Q3MetaFileParser::Parse_txmm(uint32_t chunkSize) +{ + Assert(chunkSize >= 8*4, "incorrect chunk header size"); + + uint32_t useMipmapping = f.Read(); + uint32_t pixelType = f.Read(); + uint32_t bitOrder = f.Read(); + uint32_t byteOrder = f.Read(); + uint32_t width = f.Read(); + uint32_t height = f.Read(); + uint32_t rowBytes = f.Read(); + uint32_t offset = f.Read(); + + uint32_t imageSize = rowBytes * height; + if ((imageSize & 3) != 0) + imageSize = (imageSize & 0xFFFFFFFC) + 4; + + Assert(chunkSize == 8*4 + imageSize, "incorrect chunk size"); + + + Assert(!useMipmapping, "mipmapping not supported"); + printf("%d*%d rb=%d", width, height, rowBytes); + + static const char* pixelTypeDescriptions[] = { "RGB32", "ARGB32", "RGB16", "ARGB16", "RGB16_565", "RGB24" }; + if (pixelType < kQ3PixelTypeRGB24) + printf(" %s", pixelTypeDescriptions[pixelType]); + else + printf(" UNKNOWN_PIXELTYPE"); + + Assert(offset == 0, "unsupported texture offset"); + Assert(bitOrder == kQ3EndianBig, "unsupported bit order"); + + // 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; + + + + uint32_t newTextureID = metaFile.numTextures; + + __Q3EnlargeArray(metaFile.textures, metaFile.numTextures, 'TLST'); + + metaFile.textures[newTextureID] = __Q3Alloc(1, 'PXMP'); + TQ3Pixmap& texture = *metaFile.textures[newTextureID]; + + texture.glTextureName = 0; + texture.pixelType = pixelType; + texture.bitOrder = bitOrder; + texture.byteOrder = byteOrder; + texture.width = width; + texture.height = height; + texture.pixelSize = bytesPerPixel * 8; + texture.rowBytes = trimmedRowBytes; + texture.image = __Q3Alloc(trimmedRowBytes * height, 'IMAG'); + + // Trim padding at end of rows + for (uint32_t y = 0; y < height; y++) + { + f.Read((Ptr) texture.image + y*texture.rowBytes, texture.rowBytes); + f.Skip(rowBytes - width * bytesPerPixel); + } + + // Make every pixel little-endian (especially to avoid breaking 16-bit 1-5-5-5 ARGB textures) + if (byteOrder == kQ3EndianBig) + { + ByteswapInts(bytesPerPixel, width*height, texture.image); + texture.byteOrder = kQ3EndianLittle; + } + + + Q3Pixmap_ApplyEdgePadding(&texture); + + return newTextureID; +} diff --git a/src/QD3D/QD3D.cpp b/src/QD3D/QD3D.cpp new file mode 100644 index 0000000..a12a5f5 --- /dev/null +++ b/src/QD3D/QD3D.cpp @@ -0,0 +1,342 @@ +#include "Pomme.h" +#include "PommeFiles.h" +#include "QD3D.h" +#include "3DMFInternal.h" + +#define EDGE_PADDING_REPEAT 8 + +static void Assert(bool condition, const char* message) +{ + if (!condition) + { + throw std::runtime_error(message); + } +} + +TQ3MetaFile* Q3MetaFile_Load3DMF(const FSSpec* spec) +{ + short refNum; + OSErr err; + + err = FSpOpenDF(spec, fsRdPerm, &refNum); + if (err != noErr) + return nullptr; + + TQ3MetaFile* metaFile = __Q3Alloc(1, '3DMF'); + + auto& fileStream = Pomme::Files::GetStream(refNum); + Q3MetaFileParser(fileStream, *metaFile).Parse3DMF(); + FSClose(refNum); + + return metaFile; +} + +void Q3MetaFile_Dispose(TQ3MetaFile* metaFile) +{ + __Q3GetCookie(metaFile, '3DMF'); + + for (int i = 0; i < metaFile->numTextures; i++) + { + if (metaFile->textures[i]->glTextureName) + glDeleteTextures(1, &metaFile->textures[i]->glTextureName); + Q3Pixmap_Dispose(metaFile->textures[i]); + } + + for (int i = 0; i < metaFile->numMeshes; i++) + Q3TriMeshData_Dispose(metaFile->meshes[i]); + + for (int i = 0; i < metaFile->numTopLevelGroups; i++) + __Q3Dispose(metaFile->topLevelGroups[i].meshes, 'GMSH'); + + __Q3Dispose(metaFile->textures, 'TLST'); + __Q3Dispose(metaFile->meshes, 'MLST'); + __Q3Dispose(metaFile->topLevelGroups, 'GLST'); + __Q3Dispose(metaFile, '3DMF'); +} + +#pragma mark - + + +template +static void _EdgePadding( + T* const pixelData, + const int width, + const int height, + const int rowBytes, + const T alphaMask) +{ + Assert(rowBytes % sizeof(T) == 0, "EdgePadding: rowBytes is not a multiple of pixel bytesize"); + const int rowAdvance = rowBytes / sizeof(T); + + T* const firstRow = pixelData; + T* const lastRow = firstRow + (height-1) * rowAdvance; + + for (int i = 0; i < EDGE_PADDING_REPEAT; i++) + { + // Dilate horizontally, row by row + for (T* row = firstRow; row <= lastRow; row += rowAdvance) + { + // Expand east + for (int x = 0; x < width-1; x++) + if (!row[x]) + row[x] = row[x+1] & ~alphaMask; + + // Expand west + for (int x = width-1; x > 0; x--) + if (!row[x]) + row[x] = row[x-1] & ~alphaMask; + } + + // Dilate vertically, column by column + for (int x = 0; x < width; x++) + { + // Expand south + for (T* row = firstRow; row < lastRow; row += rowAdvance) + if (!row[x]) + row[x] = row[x + rowAdvance] & ~alphaMask; + + // Expand north + for (T* row = lastRow; row > firstRow; row -= rowAdvance) + if (!row[x]) + row[x] = row[x - rowAdvance] & ~alphaMask; + } + } +} + +void Q3Pixmap_ApplyEdgePadding(TQ3Pixmap* pm) +{ + switch (pm->pixelType) + { + case kQ3PixelTypeARGB16: + Assert(pm->rowBytes >= pm->width * 2, "EdgePadding ARGB16: incorrect rowBytes"); + _EdgePadding( + (uint16_t *) pm->image, + pm->width, + pm->height, + pm->rowBytes, + pm->byteOrder==kQ3EndianBig? 0x0080: 0x8000); + break; + + case kQ3PixelTypeARGB32: + Assert(pm->rowBytes >= pm->width * 4, "EdgePadding ARGB32: incorrect rowBytes"); + _EdgePadding( + (uint32_t *) pm->image, + pm->width, + pm->height, + pm->rowBytes, + pm->byteOrder==kQ3EndianBig? 0x000000FF: 0xFF000000); + break; + + case kQ3PixelTypeRGB16: + case kQ3PixelTypeRGB16_565: + case kQ3PixelTypeRGB24: + case kQ3PixelTypeRGB32: + // Unnecessary to apply edge padding here because there's no alpha channel + break; + + default: + Assert(false, "EdgePadding: pixel type unsupported"); + break; + } +} + +void Q3Pixmap_Dispose(TQ3Pixmap* pixmap) +{ + __Q3Dispose(pixmap->image, 'IMAG'); + __Q3Dispose(pixmap, 'PXMP'); +} + +#pragma mark - + +TQ3TriMeshData* Q3TriMeshData_New(int numTriangles, int numPoints) +{ + TQ3TriMeshData* mesh = __Q3Alloc(1, 'MESH'); + + mesh->numTriangles = numTriangles; + mesh->numPoints = numPoints; + mesh->points = __Q3Alloc(numPoints, 'TMpt'); + mesh->triangles = __Q3Alloc(numTriangles, 'TMtr'); + mesh->vertexNormals = __Q3Alloc(numPoints, 'TMvn'); + mesh->vertexUVs = __Q3Alloc(numPoints, 'TMuv'); + mesh->vertexColors = nullptr; + mesh->diffuseColor = {1, 1, 1, 1}; + mesh->hasTexture = false; + mesh->textureHasTransparency = false; + + for (int i = 0; i < numPoints; i++) + { + mesh->vertexNormals[i] = {0, 1, 0}; + mesh->vertexUVs[i] = {.5f, .5f}; +// triMeshData->vertexColors[i] = {1, 1, 1, 1}; + } + + return mesh; +} + +TQ3TriMeshData* Q3TriMeshData_Duplicate(const TQ3TriMeshData* source) +{ + TQ3TriMeshData* mesh = __Q3Copy(source, 'MESH'); + mesh->points = __Q3Copy(source->points, 'TMpt'); + mesh->triangles = __Q3Copy(source->triangles, 'TMtr'); + mesh->vertexNormals = __Q3Copy(source->vertexNormals, 'TMvn'); + mesh->vertexColors = __Q3Copy(source->vertexColors, 'TMvc'); + mesh->vertexUVs = __Q3Copy(source->vertexUVs, 'TMuv'); + return mesh; +} + +void Q3TriMeshData_Dispose(TQ3TriMeshData* mesh) +{ + __Q3DisposeArray(&mesh->points, 'TMpt'); + __Q3DisposeArray(&mesh->triangles, 'TMtr'); + __Q3DisposeArray(&mesh->vertexNormals, 'TMvn'); + __Q3DisposeArray(&mesh->vertexColors, 'TMvc'); + __Q3DisposeArray(&mesh->vertexUVs, 'TMuv'); + __Q3Dispose(mesh, 'MESH'); +} + +void Q3TriMeshData_SubdivideTriangles(TQ3TriMeshData* mesh) +{ + struct Edge + { + int a; + int b; + int midpoint; + }; + + std::map edges; + + const int oldNumTriangles = mesh->numTriangles; + const int oldNumPoints = mesh->numPoints; + + auto triangleEdges = new Edge*[oldNumTriangles * 3]; + int numDistinctEdges = 0; + + //------------------------------------------------------------------------- + // Prep edge records (so we have edge count too) + + for (int t = 0; t < mesh->numTriangles; t++) + { + auto& triangle = mesh->triangles[t]; + + for (int e = 0; e < 3; e++) + { + int edgeP0 = triangle.pointIndices[e]; + int edgeP1 = triangle.pointIndices[(e+1) % 3]; + if (edgeP0 > edgeP1) + { + int swap = edgeP0; + edgeP0 = edgeP1; + edgeP1 = swap; + } + uint32_t edgeHash = (edgeP0 << 16) | edgeP1; + + auto foundEdge = edges.find(edgeHash); + if (foundEdge != edges.end()) + { + triangleEdges[t*3 + e] = &foundEdge->second; + } + else + { + edges[edgeHash] = { edgeP0, edgeP1, -1 }; + triangleEdges[t*3 + e] = &edges[edgeHash]; + numDistinctEdges++; + } + } + } + + //------------------------------------------------------------------------- + // Reallocate mesh + + int numPointsWritten = oldNumPoints; + int numTrianglesWritten = oldNumTriangles; + + mesh->numTriangles *= 4; + mesh->numPoints += numDistinctEdges; + + mesh->points = __Q3Realloc(mesh->points, mesh->numPoints, 'TMpt'); + mesh->vertexUVs = __Q3Realloc(mesh->vertexUVs, mesh->numPoints, 'TMuv'); + mesh->vertexNormals = __Q3Realloc(mesh->vertexNormals, mesh->numPoints, 'TMvn'); + if (mesh->vertexColors) + mesh->vertexColors = __Q3Realloc(mesh->vertexColors, mesh->numPoints, 'TMvc'); + + mesh->triangles = __Q3Realloc(mesh->triangles, mesh->numTriangles, 'TMtr'); + + //------------------------------------------------------------------------- + // Create edge midpoints + + for (auto& edgeKV: edges) + { + auto& edge = edgeKV.second; + + edge.midpoint = numPointsWritten; + numPointsWritten++; + + int M = edge.midpoint; + int A = edge.a; + int B = edge.b; + + mesh->points[M].x = (mesh->points[A].x + mesh->points[B].x) / 2.0f; + mesh->points[M].y = (mesh->points[A].y + mesh->points[B].y) / 2.0f; + mesh->points[M].z = (mesh->points[A].z + mesh->points[B].z) / 2.0f; + + mesh->vertexNormals[M].x = (mesh->vertexNormals[A].x + mesh->vertexNormals[B].x) / 2.0f; + mesh->vertexNormals[M].y = (mesh->vertexNormals[A].y + mesh->vertexNormals[B].y) / 2.0f; + mesh->vertexNormals[M].z = (mesh->vertexNormals[A].z + mesh->vertexNormals[B].z) / 2.0f; + + mesh->vertexUVs[M].u = (mesh->vertexUVs[A].u + mesh->vertexUVs[B].u) / 2.0f; + mesh->vertexUVs[M].v = (mesh->vertexUVs[A].v + mesh->vertexUVs[B].v) / 2.0f; + + if (mesh->vertexColors) + { + mesh->vertexColors[M].r = (mesh->vertexColors[A].r + mesh->vertexColors[B].r) / 2.0f; + mesh->vertexColors[M].g = (mesh->vertexColors[A].g + mesh->vertexColors[B].g) / 2.0f; + mesh->vertexColors[M].b = (mesh->vertexColors[A].b + mesh->vertexColors[B].b) / 2.0f; + mesh->vertexColors[M].a = (mesh->vertexColors[A].a + mesh->vertexColors[B].a) / 2.0f; + } + } + + //------------------------------------------------------------------------- + // Create new triangles + + for (int t = 0; t < oldNumTriangles; t++) + { + auto& triangle = mesh->triangles[t]; + + /* + Original triangle: ABC. + Create 4 new triangles. + + B + + + / \ + / \ + / \ + >+ . . . +< + / . . \ + / . . \ + / . . \ + +-------+-------+ + A ^ C + */ + + uint16_t A = triangle.pointIndices[0]; + uint16_t B = triangle.pointIndices[1]; + uint16_t C = triangle.pointIndices[2]; + uint16_t A2B = triangleEdges[t*3 + 0]->midpoint; + uint16_t B2C = triangleEdges[t*3 + 1]->midpoint; + uint16_t C2A = triangleEdges[t*3 + 2]->midpoint; + + mesh->triangles[numTrianglesWritten++] = {{A2B, B, B2C} }; + mesh->triangles[numTrianglesWritten++] = {{B2C, C, C2A} }; + mesh->triangles[numTrianglesWritten++] = {{C2A, A, A2B} }; + mesh->triangles[t] = {{A2B, B2C, C2A} }; // replace original triangle + } + + //------------------------------------------------------------------------- + // Make sure we didn't break the mesh + + delete[] triangleEdges; + + Assert(numTrianglesWritten == 4*oldNumTriangles, "unexpected number of triangles written"); + Assert(numPointsWritten == oldNumPoints + (int)edges.size(), "unexpected number of points written"); +} diff --git a/src/QD3D/QD3D.h b/src/QD3D/QD3D.h new file mode 100644 index 0000000..2595d6b --- /dev/null +++ b/src/QD3D/QD3D.h @@ -0,0 +1,345 @@ +#pragma once + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include "PommeTypes.h" +#include +#include + +typedef float TQ3Float32; +typedef uint32_t TQ3Uns32; +typedef int32_t TQ3Int32; +typedef int32_t TQ3ObjectType; + +// junk types +typedef void* TQ3TransformObject; +typedef void* TQ3Object; +typedef void* TQ3GroupPosition; +typedef void* TQ3GeometryObject; +typedef void* TQ3ViewObject; +typedef void* TQ3SurfaceShaderObject; +typedef void* TQ3StorageObject; +typedef void* TQ3ShaderObject; +typedef void* TQ3FileObject; +typedef void* TQ3TriMeshAttributeData; + +typedef enum +{ + kQ3False = 0, + kQ3True = 1, + kQ3BooleanSize32 = 0xFFFFFFFF +} TQ3Boolean; + +typedef enum +{ + kQ3Off = 0, + kQ3On = 1, + kQ3SwitchSize32 = 0xFFFFFFFF +} TQ3Switch; + +typedef enum +{ + kQ3Failure = 0, + kQ3Success = 1, + kQ3StatusSize32 = 0xFFFFFFFF +} TQ3Status; + + +enum TQ3AttributeTypes +{ + kQ3AttributeTypeNone = 0, // N/A + kQ3AttributeTypeSurfaceUV = 1, // TQ3Param2D + kQ3AttributeTypeShadingUV = 2, // TQ3Param2D + kQ3AttributeTypeNormal = 3, // TQ3Vector3D + kQ3AttributeTypeAmbientCoefficient = 4, // float + kQ3AttributeTypeDiffuseColor = 5, // TQ3ColorRGB + kQ3AttributeTypeSpecularColor = 6, // TQ3ColorRGB + kQ3AttributeTypeSpecularControl = 7, // float + kQ3AttributeTypeTransparencyColor = 8, // TQ3ColorRGB + kQ3AttributeTypeSurfaceTangent = 9, // TQ3Tangent2D + kQ3AttributeTypeHighlightState = 10, // TQ3Switch + kQ3AttributeTypeSurfaceShader = 11, // TQ3SurfaceShaderObject + kQ3AttributeTypeEmissiveColor = 12, // TQ3ColorRGB + kQ3AttributeTypeNumTypes = 13, // N/A + kQ3AttributeTypeSize32 = 0xFFFFFFFF +}; + +typedef enum +{ + /// 8 bits for red, green, and blue. High-order byte ignored. + kQ3PixelTypeRGB32 = 0, + + /// 8 bits for alpha, red, green, and blue. + kQ3PixelTypeARGB32 = 1, + + /// 5 bits for red, green, and blue. High-order bit ignored. + kQ3PixelTypeRGB16 = 2, + + /// 1 bit for alpha. 5 bits for red, green, and blue. + kQ3PixelTypeARGB16 = 3, + + /// 5 bits for red, 6 bits for green, 5 bits for blue. + kQ3PixelTypeRGB16_565 = 4, + + /// 8 bits for red, green, and blue. No alpha byte. + kQ3PixelTypeRGB24 = 5, + + kQ3PixelTypeUnknown = 200, + kQ3PixelTypeSize32 = 0xFFFFFFFF +} TQ3PixelType; + +typedef enum TQ3InterpolationStyle +{ + kQ3InterpolationStyleNone = 0, + kQ3InterpolationStyleVertex = 1, + kQ3InterpolationStylePixel = 2, + kQ3InterpolationSize32 = 0xFFFFFFFF +} TQ3InterpolationStyle; + +typedef enum TQ3BackfacingStyle +{ + kQ3BackfacingStyleBoth = 0, + kQ3BackfacingStyleRemove = 1, + kQ3BackfacingStyleFlip = 2, + kQ3BackfacingStyleRemoveFront = 3, + kQ3BackfacingStyleSize32 = 0xFFFFFFFF +} TQ3BackfacingStyle; + +typedef enum TQ3FillStyle +{ + kQ3FillStyleFilled = 0, + kQ3FillStyleEdges = 1, + kQ3FillStylePoints = 2, + kQ3FillStyleSize32 = 0xFFFFFFFF +} TQ3FillStyle; + +typedef enum TQ3FogMode +{ + kQ3FogModeLinear = 0, + kQ3FogModeExponential = 1, + kQ3FogModeExponentialSquared = 2, + kQ3FogModeAlpha = 3, + kQ3FogModePlaneBasedLinear = 4, + kQ3FogModeSize32 = 0xFFFFFFFF +} TQ3FogMode; + +typedef enum +{ + kQ3EndianBig = 0, + kQ3EndianLittle = 1, + kQ3EndianSize32 = 0xFFFFFFFF +} TQ3Endian; + +typedef struct TQ3Param2D +{ + float u; + float v; +} TQ3Param2D; + +typedef struct TQ3Point2D +{ + float x; + float y; +} TQ3Point2D; + +typedef struct TQ3Area +{ + TQ3Point2D min; + TQ3Point2D max; +} TQ3Area; + +typedef struct TQ3Point3D +{ + float x; + float y; + float z; +} TQ3Point3D; + +typedef struct TQ3Vector2D +{ + float x; + float y; +} TQ3Vector2D; + +typedef struct TQ3Vector3D +{ + float x; + float y; + float z; +} TQ3Vector3D; + +typedef struct TQ3RationalPoint3D +{ + float x; + float y; + float w; +} TQ3RationalPoint3D; + +typedef struct TQ3RationalPoint4D +{ + float x; + float y; + float z; + float w; +} TQ3RationalPoint4D; + +typedef struct TQ3ColorARGB +{ + float a; + float r; + float g; + float b; +} TQ3ColorARGB; + +typedef struct TQ3ColorRGBA +{ + float r; + float g; + float b; + float a; +} TQ3ColorRGBA; + +typedef struct TQ3ColorRGB +{ + float r; + float g; + float b; +} TQ3ColorRGB; + +typedef struct TQ3Vertex3D +{ + TQ3Point3D point; +} TQ3Vertex3D; + +typedef struct TQ3BoundingBox +{ + TQ3Point3D min; + TQ3Point3D max; + TQ3Boolean isEmpty; +} TQ3BoundingBox; + +typedef struct TQ3Matrix3x3 +{ + float value[3][3]; +} TQ3Matrix3x3; + +typedef struct TQ3Matrix4x4 +{ + float value[4][4]; +} TQ3Matrix4x4; + +typedef struct TQ3PlaneEquation +{ + TQ3Vector3D normal; + float constant; +} TQ3PlaneEquation; + +// WARNING: this structure differs from QD3D (indices were originally 32-bit) +typedef struct TQ3TriMeshTriangleData +{ + uint16_t pointIndices[3]; +} TQ3TriMeshTriangleData; + +// This structure differs from QD3D. +typedef struct TQ3TriMeshData +{ + int numTriangles; + TQ3TriMeshTriangleData *triangles; + + int numPoints; + TQ3Point3D *points; + TQ3Vector3D *vertexNormals; + TQ3Param2D *vertexUVs; // automatically allocated by constructor + TQ3ColorRGBA *vertexColors; // may be null if mesh doesn't need per-vertex colors (Nanosaur never does) + + TQ3BoundingBox bBox; + + bool hasTexture; + bool textureHasTransparency; // TODO + int internalTextureID; + uint32_t glTextureName; + + bool hasVertexColors; + + TQ3ColorRGBA diffuseColor; +} TQ3TriMeshData; + +// This structure does not exist in QD3D. +typedef struct TQ3TriMeshFlatGroup +{ + int numMeshes; + TQ3TriMeshData** meshes; +} TQ3TriMeshFlatGroup; + +typedef struct TQ3FogStyleData +{ + TQ3Switch state; + TQ3FogMode mode; + float fogStart; + float fogEnd; + float density; + TQ3ColorRGBA color; +} TQ3FogStyleData; + +typedef struct TQ3CameraPlacement +{ + TQ3Point3D cameraLocation; + TQ3Point3D pointOfInterest; + TQ3Vector3D upVector; +} TQ3CameraPlacement; + +// WARNING: this structure differs from QD3D. +typedef struct TQ3Pixmap +{ + uint8_t *image; + uint32_t width; + uint32_t height; + uint32_t rowBytes; + uint32_t pixelSize; + uint32_t pixelType; + uint32_t bitOrder; + uint32_t byteOrder; + uint32_t glTextureName; +} TQ3Pixmap; + +// WARNING: this structure does not exist in QD3D. +typedef struct TQ3MetaFile +{ + int numTextures; + TQ3Pixmap **textures; + + int numMeshes; + TQ3TriMeshData **meshes; + + int numTopLevelGroups; + TQ3TriMeshFlatGroup *topLevelGroups; +} TQ3MetaFile; + +#pragma mark - + +TQ3MetaFile* Q3MetaFile_Load3DMF(const FSSpec* spec); + +void Q3MetaFile_Dispose(TQ3MetaFile* the3DMFFile); + +#pragma mark - + +void Q3Pixmap_ApplyEdgePadding(TQ3Pixmap*); + +void Q3Pixmap_Dispose(TQ3Pixmap*); + +#pragma mark - + +TQ3TriMeshData* Q3TriMeshData_New(int numTriangles, int numPoints); + +TQ3TriMeshData* Q3TriMeshData_Duplicate(const TQ3TriMeshData* source); + +void Q3TriMeshData_Dispose(TQ3TriMeshData*); + +void Q3TriMeshData_SubdivideTriangles(TQ3TriMeshData* src); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/src/QD3D/QD3DMath.cpp b/src/QD3D/QD3DMath.cpp new file mode 100644 index 0000000..4512dbb --- /dev/null +++ b/src/QD3D/QD3DMath.cpp @@ -0,0 +1,384 @@ +/* + +Adapted from Quesa's math routines. +Original copyright notice below: + +Copyright (c) 1999-2020, Quesa Developers. All rights reserved. + +For the current release of Quesa, please see: + + +Redistribution and use in source and binary forms, with or without modification, are permitted +provided that the following conditions are met: + +o Redistributions of source code must retain the above copyright notice, this list of conditions +and the following disclaimer. + +o 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. + +o Neither the name of Quesa nor the names of its contributors may be used to endorse or promote +products derived from this software without specific prior written permission. + +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 "QD3DMath.h" +#include + +void Q3Point3D_To3DTransformArray( + const TQ3Point3D *inPoints3D, + const TQ3Matrix4x4 *matrix4x4, + TQ3Point3D *outPoints3D, + TQ3Uns32 numPoints) +{ + TQ3Uns32 i; + + // In the common case of the last column of the matrix being (0, 0, 0, 1), + // we can avoid some divisions and conditionals inside the loop. + if ( (matrix4x4->value[3][3] == 1.0f) && + (matrix4x4->value[0][3] == 0.0f) && + (matrix4x4->value[1][3] == 0.0f) && + (matrix4x4->value[2][3] == 0.0f) ) + { + for (i = 0; i < numPoints; ++i) + { + Q3Point3D_TransformAffine( inPoints3D, matrix4x4, outPoints3D ); + inPoints3D++; + outPoints3D++; + } + } + else + { + // Transform the points - will be in-lined in release builds + for (i = 0; i < numPoints; ++i) + { + Q3Point3D_Transform(inPoints3D, matrix4x4, outPoints3D); + inPoints3D++; + outPoints3D++; + } + } +} + +//----------------------------------------------------------------------------- +#pragma mark Q3Matrix4x4 + +TQ3Matrix4x4* Q3Matrix4x4_Transpose( + const TQ3Matrix4x4 *matrix4x4, + TQ3Matrix4x4 *result) +{ + int i, j; + + if (result != matrix4x4) + { + for (i = 0; i < 4; ++i) + for (j = 0; j < 4; ++j) + result->value[i][j] = matrix4x4->value[j][i]; + } + else + { + __Q3Float_Swap(result->value[1][0], result->value[0][1]); + __Q3Float_Swap(result->value[2][0], result->value[0][2]); + __Q3Float_Swap(result->value[3][0], result->value[0][3]); + __Q3Float_Swap(result->value[2][1], result->value[1][2]); + __Q3Float_Swap(result->value[3][1], result->value[1][3]); + __Q3Float_Swap(result->value[2][3], result->value[3][2]); + } + return(result); +} + + +//============================================================================= +// e3matrix4x4_extract3x3 : Select the upper left 3x3 of a 4x4 matrix +//----------------------------------------------------------------------------- +static void e3matrix4x4_extract3x3( const TQ3Matrix4x4& in4x4, TQ3Matrix3x3& out3x3 ) +{ + for (int row = 0; row < 3; ++row) + { + for (int col = 0; col < 3; ++col) + { + out3x3.value[row][col] = in4x4.value[row][col]; + } + } +} + +//============================================================================= +// e3matrix3x3_invert : Transforms the given 3x3 matrix into its inverse. +//----------------------------------------------------------------------------- +// Note : This function uses Gauss-Jordon elimination with full pivoting +// to transform the given matrix to the identity matrix while +// transforming the identity matrix to the inverse. As the given +// matrix is reduced to 1's and 0's column-by-column, the inverse +// matrix is created in its place column-by-column. +// +// See Press, et al., "Numerical Recipes in C", 2nd ed., pp. 32 ff. +//----------------------------------------------------------------------------- +static void e3matrix3x3_invert(TQ3Matrix3x3* a) +{ +#define A(x,y) a->value[x][y] + + TQ3Int32 irow = 0, icol = 0; + TQ3Int32 i, j, k; // *** WARNING: 'k' must be a SIGNED integer *** + float big, element; + TQ3Int32 ipiv[3], indxr[3], indxc[3]; + + // Initialize ipiv: ipiv[j] is 0 (1) if row/column j has not (has) been pivoted + for (j = 0; j < 3; ++j) + ipiv[j] = 0; + + // Loop over 3 pivots + for (k = 0; k < 3; ++k) + { + // Search unpivoted part of matrix for largest element to pivot on + big = -1.0f; + for (i = 0; i < 3; ++i) + { + if (ipiv[i]) + continue; + + for (j = 0; j < 3; ++j) + { + if (ipiv[j]) + continue; + + // Calculate absolute value of current element + element = A(i,j); + if (element < 0.0f) + element = -element; + + // Compare current element to largest element so far + if (element > big) + { + big = element; + irow = i; + icol = j; + } + } + } + + // If largest element is 0, the matrix is singular + // (If there are "nan" values in the matrix, "big" may still be -1.0.) + if (big <= 0.0f) + { + throw std::runtime_error("e3matrix3x3_invert: non-invertible matrix"); + return; + } + + // Mark pivot row and column + ++ipiv[icol]; + indxr[k] = irow; + indxc[k] = icol; + + // If necessary, exchange rows to put pivot element on diagonal + if (irow != icol) + { + for (j = 0; j < 3; ++j) + __Q3Float_Swap(A(irow,j), A(icol,j)); + } + + // Divide pivot row by pivot element + // + // Note: If we were dividing by the same element many times, it would + // make sense to multiply by its inverse. Since we divide by a given + // elemen only 3 (4) times for a 3x3 (4x4) matrix, it doesn't make sense + // to pay for the extra floating-point operation. + element = A(icol,icol); + A(icol,icol) = 1.0f; // overwrite original matrix with inverse + for (j = 0; j < 3; ++j) + A(icol,j) /= element; + + // Reduce other rows + for (i = 0; i < 3; ++i) + { + if (i == icol) + continue; + + element = A(i,icol); + A(i,icol) = 0.0f; // overwrite original matrix with inverse + for (j = 0; j < 3; ++j) + A(i,j) -= A(icol,j)*element; + } + } + + // Permute columns + for (k = 3; --k >= 0; ) // *** WARNING: 'k' must be a SIGNED integer *** + { + if (indxr[k] != indxc[k]) + { + for (i = 0; i < 3; ++i) + __Q3Float_Swap(A(i,indxr[k]), A(i,indxc[k])); + } + } + +#undef A +} + +//============================================================================= +// e3matrix4x4_invert : Transforms the given 4x4 matrix into its inverse. +//----------------------------------------------------------------------------- +// Note : This function uses Gauss-Jordon elimination with full pivoting +// to transform the given matrix to the identity matrix while +// transforming the identity matrix to the inverse. As the given +// matrix is reduced to 1's and 0's column-by-column, the inverse +// matrix is created in its place column-by-column. +// +// See Press, et al., "Numerical Recipes in C", 2nd ed., pp. 32 ff. +//----------------------------------------------------------------------------- +static void e3matrix4x4_invert(TQ3Matrix4x4* a) +{ +#define A(x,y) a->value[x][y] + + TQ3Int32 irow = 0, icol = 0; + TQ3Int32 i, j, k; // *** WARNING: 'k' must be a SIGNED integer *** + float big, element; + TQ3Int32 ipiv[4], indxr[4], indxc[4]; + + // Initialize ipiv: ipiv[j] is 0 (1) if row/column j has not (has) been pivoted + for (j = 0; j < 4; ++j) + ipiv[j] = 0; + + // Loop over 4 pivots + for (k = 0; k < 4; ++k) + { + // Search unpivoted part of matrix for largest element to pivot on + big = -1.0f; + for (i = 0; i < 4; ++i) + { + if (ipiv[i]) + continue; + + for (j = 0; j < 4; ++j) + { + if (ipiv[j]) + continue; + + // Calculate absolute value of current element + element = A(i,j); + if (element < 0.0f) + element = -element; + + // Compare current element to largest element so far + if (element > big) + { + big = element; + irow = i; + icol = j; + } + } + } + + // If largest element is 0, the matrix is singular + // (If there are "nan" values in the matrix, "big" may still be -1.0.) + if (big <= 0.0f) + { + throw std::runtime_error("e3matrix4x4_invert: non-invertible matrix"); + return; + } + + // Mark pivot row and column + ++ipiv[icol]; + indxr[k] = irow; + indxc[k] = icol; + + // If necessary, exchange rows to put pivot element on diagonal + if (irow != icol) + { + for (j = 0; j < 4; ++j) + __Q3Float_Swap(A(irow,j), A(icol,j)); + } + + // Divide pivot row by pivot element + // + // Note: If we were dividing by the same element many times, it would + // make sense to multiply by its inverse. Since we divide by a given + // element only 3 (4) times for a 3x3 (4x4) matrix, it doesn't make sense + // to pay for the extra floating-point operation. + element = A(icol,icol); + A(icol,icol) = 1.0f; // overwrite original matrix with inverse + for (j = 0; j < 4; ++j) + A(icol,j) /= element; + + // Reduce other rows + for (i = 0; i < 4; ++i) + { + if (i == icol) + continue; + + element = A(i,icol); + A(i,icol) = 0.0f; // overwrite original matrix with inverse + for (j = 0; j < 4; ++j) + A(i,j) -= A(icol,j)*element; + } + } + + // Permute columns + for (k = 4; --k >= 0; ) // *** WARNING: 'k' must be a SIGNED integer *** + { + if (indxr[k] != indxc[k]) + { + for (i = 0; i < 4; ++i) + __Q3Float_Swap(A(i,indxr[k]), A(i,indxc[k])); + } + } + +#undef A +} + +TQ3Matrix4x4* Q3Matrix4x4_Invert( + const TQ3Matrix4x4 *matrix4x4, + TQ3Matrix4x4 *result) +{ + if (result != matrix4x4) + *result = *matrix4x4; + + // The 4x4 matrices used in 3D graphics often have a last column of + // (0, 0, 0, 1). In that case, we want the inverse to have exactly the same + // last column, and we can compute the inverse with fewer floating point + // multiplies and divides. The inverse of the matrix + // A 0 + // v 1 + // (where A is 3x3 and v is 1x3) is + // inv(A) 0 + // -v * inv(A) 1 . + if ( (result->value[3][3] == 1.0f) && (result->value[0][3] == 0.0f) && + (result->value[1][3] == 0.0f) && (result->value[2][3] == 0.0f) ) + { + TQ3Matrix3x3 upperLeft; + e3matrix4x4_extract3x3( *result, upperLeft ); + int i, j; + + e3matrix3x3_invert( &upperLeft ); + + for (i = 0; i < 3; ++i) + { + for (j = 0; j < 3; ++j) + { + result->value[i][j] = upperLeft.value[i][j]; + } + } + + TQ3RationalPoint3D v = { + result->value[3][0], result->value[3][1], result->value[3][2] + }; + Q3RationalPoint3D_Transform( &v, &upperLeft, &v ); + + result->value[3][0] = -v.x; + result->value[3][1] = -v.y; + result->value[3][2] = -v.w; + } + else + { + e3matrix4x4_invert(result); + } + + return(result); +} diff --git a/src/QD3D/QD3DMath.h b/src/QD3D/QD3DMath.h new file mode 100644 index 0000000..578a309 --- /dev/null +++ b/src/QD3D/QD3DMath.h @@ -0,0 +1,615 @@ +/* + +Adapted from Quesa's math routines. +Original copyright notice below: + +Copyright (c) 1999-2020, Quesa Developers. All rights reserved. + +For the current release of Quesa, please see: + + +Redistribution and use in source and binary forms, with or without modification, are permitted +provided that the following conditions are met: + +o Redistributions of source code must retain the above copyright notice, this list of conditions +and the following disclaimer. + +o 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. + +o Neither the name of Quesa nor the names of its contributors may be used to endorse or promote +products derived from this software without specific prior written permission. + +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. + +*/ + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include "QD3D.h" +#include +#include + +#ifdef FLT_EPSILON + #define kQ3RealZero (FLT_EPSILON) +#else + #define kQ3RealZero ((TQ3Float32) 1.19209290e-07) +#endif + +#ifdef FLT_MAX + #define kQ3MaxFloat (FLT_MAX) +#else + #define kQ3MaxFloat ((TQ3Float32) 3.40282347e+38) +#endif + +#ifdef FLT_MIN + #define kQ3MinFloat (FLT_MIN) +#else + #define kQ3MinFloat ((TQ3Float32) 1.17549e-38) +#endif + +#define kQ3Pi ((TQ3Float32) 3.1415926535898) +#define kQ32Pi ((TQ3Float32) (2.0 * 3.1415926535898)) +#define kQ3PiOver2 ((TQ3Float32) (3.1415926535898 / 2.0)) +#define kQ33PiOver2 ((TQ3Float32) (3.0 * 3.1415926535898 / 2.0)) + +#define Q3Math_DegreesToRadians(_x) ((TQ3Float32) ((_x) * kQ3Pi / 180.0f)) +#define Q3Math_RadiansToDegrees(_x) ((TQ3Float32) ((_x) * 180.0f / kQ3Pi)) +#define Q3Math_Min(_x,_y) ((_x) <= (_y) ? (_x) : (_y)) +#define Q3Math_Max(_x,_y) ((_x) >= (_y) ? (_x) : (_y)) + +#define __Q3Memory_Clear(ptr, size) memset((ptr), 0, (size)) + +#define __Q3Float_Swap(_a, _b) \ + do { \ + float _temp; \ + \ + _temp = (_a); \ + (_a) = (_b); \ + (_b) = _temp; \ + } while (0) + +//----------------------------------------------------------------------------- +#pragma mark Q3Point2D + +static inline float Q3Point2D_DistanceSquared( + const TQ3Point2D *p1, + const TQ3Point2D *p2) +{ + return ((p1->x - p2->x) * (p1->x - p2->x)) + + ((p1->y - p2->y) * (p1->y - p2->y)); +} + +static inline float Q3Point2D_Distance( + const TQ3Point2D *p1, + const TQ3Point2D *p2) +{ + return sqrtf(Q3Point2D_DistanceSquared(p1, p2)); +} + +//----------------------------------------------------------------------------- +#pragma mark Q3Point3D + +static inline float Q3Point3D_DistanceSquared( + const TQ3Point3D *p1, + const TQ3Point3D *p2) +{ + return ((p1->x - p2->x) * (p1->x - p2->x)) + + ((p1->y - p2->y) * (p1->y - p2->y)) + + ((p1->z - p2->z) * (p1->z - p2->z)); +} + +static inline float Q3Point3D_Distance( + const TQ3Point3D *p1, + const TQ3Point3D *p2) +{ + return sqrtf(Q3Point3D_DistanceSquared(p1, p2)); +} + +static inline void Q3Point3D_CrossProductTri( + const TQ3Point3D* p1, + const TQ3Point3D* p2, + const TQ3Point3D* p3, + TQ3Point3D* result) +{ + float v1_x = p2->x - p1->x; + float v1_y = p2->y - p1->y; + float v1_z = p2->z - p1->z; + + float v2_x = p3->x - p2->x; + float v2_y = p3->y - p2->y; + float v2_z = p3->z - p2->z; + + result->x = (v1_y * v2_z) - (v1_z * v2_y); + result->y = (v1_z * v2_x) - (v1_x * v2_z); + result->z = (v1_x * v2_y) - (v1_y * v2_x); +} + +static inline TQ3Vector3D* Q3Vector3D_Transform( + const TQ3Vector3D *vector3D, + const TQ3Matrix4x4 *matrix4x4, + TQ3Vector3D *result) +{ + // Save input to avoid problems when result is same as input + float x = vector3D->x; + float y = vector3D->y; + float z = vector3D->z; + +#define M(x,y) matrix4x4->value[x][y] + result->x = x*M(0,0) + y*M(1,0) + z*M(2,0); + result->y = x*M(0,1) + y*M(1,1) + z*M(2,1); + result->z = x*M(0,2) + y*M(1,2) + z*M(2,2); +#undef M + + return result; +} + +static inline TQ3Point3D* Q3Point3D_Transform( + const TQ3Point3D *point3D, + const TQ3Matrix4x4 *matrix4x4, + TQ3Point3D *result) +{ + // Save input to avoid problems when result is same as input + float x = point3D->x; + float y = point3D->y; + float z = point3D->z; + float neww; + +#define M(x,y) matrix4x4->value[x][y] + result->x = x*M(0,0) + y*M(1,0) + z*M(2,0) + M(3,0); + result->y = x*M(0,1) + y*M(1,1) + z*M(2,1) + M(3,1); + result->z = x*M(0,2) + y*M(1,2) + z*M(2,2) + M(3,2); + neww = x*M(0,3) + y*M(1,3) + z*M(2,3) + M(3,3); +#undef M + + if (neww == 0.0f) + { +// E3ErrorManager_PostError( kQ3ErrorInfiniteRationalPoint, kQ3False ); + neww = 1.0f; + } + + if (neww != 1.0f) + { + float invw = 1.0f / neww; + result->x *= invw; + result->y *= invw; + result->z *= invw; + } + + return result; +} + +static inline TQ3Point3D* Q3Point3D_TransformAffine( + const TQ3Point3D *point3D, + const TQ3Matrix4x4 *matrix4x4, + TQ3Point3D *result) +{ + // Save input to avoid problems when result is same as input + float x = point3D->x; + float y = point3D->y; + float z = point3D->z; + +#define M(x,y) matrix4x4->value[x][y] + result->x = x*M(0,0) + y*M(1,0) + z*M(2,0) + M(3,0); + result->y = x*M(0,1) + y*M(1,1) + z*M(2,1) + M(3,1); + result->z = x*M(0,2) + y*M(1,2) + z*M(2,2) + M(3,2); +#undef M + + return(result); +} + +void Q3Point3D_To3DTransformArray( + const TQ3Point3D *inPoints3D, + const TQ3Matrix4x4 *matrix4x4, + TQ3Point3D *outPoints3D, + TQ3Uns32 numPoints); + +//----------------------------------------------------------------------------- +#pragma mark Q3RationalPoint3D + +static inline TQ3RationalPoint3D* Q3RationalPoint3D_Transform( + const TQ3RationalPoint3D *rationalPoint3D, + const TQ3Matrix3x3 *matrix3x3, + TQ3RationalPoint3D *result) +{ + // Save input to avoid problems when result is same as input + float x = rationalPoint3D->x; + float y = rationalPoint3D->y; + float w = rationalPoint3D->w; + +#define M(x,y) matrix3x3->value[x][y] + result->x = x*M(0,0) + y*M(1,0) + w*M(2,0); + result->y = x*M(0,1) + y*M(1,1) + w*M(2,1); + result->w = x*M(0,2) + y*M(1,2) + w*M(2,2); +#undef M + + return(result); +} + +//----------------------------------------------------------------------------- +#pragma mark Q3Vector2D + +static inline float Q3Vector2D_LengthSquared( + const TQ3Vector2D* v) +{ + return (v->x * v->x) + (v->y * v->y); +} + +static inline float Q3Vector2D_Length( + const TQ3Vector2D* v) +{ + return sqrtf(Q3Vector2D_LengthSquared(v)); +} + +static inline void Q3Vector2D_Scale( + const TQ3Vector2D* v1, + float s, + TQ3Vector2D* result) +{ + result->x = v1->x * s; + result->y = v1->y * s; +} + +static inline void Q3Vector2D_Normalize( + const TQ3Vector2D* v1, + TQ3Vector2D* result) +{ + // Normalization performance trick: usually, adding kQ3MinFloat to the length + // makes no difference, but it prevents division by zero without a branch. + float theLength = Q3Vector2D_Length(v1) + kQ3MinFloat; + Q3Vector2D_Scale(v1, 1.0f / theLength, result); +} + +//----------------------------------------------------------------------------- +#pragma mark Q3Vector3D + +static inline float Q3Vector3D_LengthSquared( + const TQ3Vector3D* v) +{ + return (v->x * v->x) + (v->y * v->y) + (v->z * v->z); +} + +static inline float Q3Vector3D_Length( + const TQ3Vector3D* v) +{ + return sqrtf(Q3Vector3D_LengthSquared(v)); +} + +static inline float Q3Vector2D_Dot( + const TQ3Vector2D *v1, + const TQ3Vector2D *v2) +{ + return (v1->x * v2->x) + (v1->y * v2->y); +} + +static inline void Q3Vector3D_Cross( + const TQ3Vector3D* v1, + const TQ3Vector3D* v2, + TQ3Vector3D* result) +{ + float rx = (v1->y * v2->z) - (v1->z * v2->y); + float ry = (v1->z * v2->x) - (v1->x * v2->z); + float rz = (v1->x * v2->y) - (v1->y * v2->x); + + result->x = rx; + result->y = ry; + result->z = rz; +} + +static inline float Q3Vector3D_Dot( + const TQ3Vector3D *v1, + const TQ3Vector3D *v2) +{ + return (v1->x * v2->x) + + (v1->y * v2->y) + + (v1->z * v2->z); +} + +static inline void Q3Vector3D_Scale( + const TQ3Vector3D *v1, + float scale, + TQ3Vector3D *result) +{ + result->x = v1->x * scale; + result->y = v1->y * scale; + result->z = v1->z * scale; +} + +static inline void Q3Vector3D_Normalize( + const TQ3Vector3D* v1, + TQ3Vector3D* result) +{ + float theLength = Q3Vector3D_Length(v1) + kQ3MinFloat; + Q3Vector3D_Scale(v1, 1.0f / theLength, result); +} + +//----------------------------------------------------------------------------- +#pragma mark Q3Matrix3x3 + +static inline TQ3Matrix3x3* Q3Matrix3x3_SetIdentity( + TQ3Matrix3x3 *matrix3x3) +{ + __Q3Memory_Clear(matrix3x3, sizeof(TQ3Matrix3x3)); + +#define M(x,y) matrix3x3->value[x][y] + M(0,0) = 1.0f; + M(1,1) = 1.0f; + M(2,2) = 1.0f; +#undef M + + return matrix3x3; +} + +static inline TQ3Matrix3x3* Q3Matrix3x3_SetTranslate( + TQ3Matrix3x3* matrix3x3, + float xTrans, + float yTrans) +{ + __Q3Memory_Clear(matrix3x3, sizeof(TQ3Matrix3x3)); + +#define M(x,y) matrix3x3->value[x][y] + M(0,0) = 1.0f; + + M(1,1) = 1.0f; + + M(2,0) = xTrans; + M(2,1) = yTrans; + M(2,2) = 1.0f; +#undef M + + return matrix3x3; +} + +//----------------------------------------------------------------------------- +#pragma mark Q3Matrix4x4 + +static inline TQ3Matrix4x4* Q3Matrix4x4_SetIdentity( + TQ3Matrix4x4 *matrix4x4) +{ + __Q3Memory_Clear(matrix4x4, sizeof(TQ3Matrix4x4)); + +#define M(x,y) matrix4x4->value[x][y] + + M(0,0) = 1.0f; + M(1,1) = 1.0f; + M(2,2) = 1.0f; + M(3,3) = 1.0f; + +#undef M + + return matrix4x4; +} + +static inline TQ3Matrix4x4* Q3Matrix4x4_SetScale( + TQ3Matrix4x4 *matrix4x4, + float xScale, + float yScale, + float zScale) +{ + __Q3Memory_Clear(matrix4x4, sizeof(TQ3Matrix4x4)); + +#define M(x,y) matrix4x4->value[x][y] + + M(0,0) = xScale; + M(1,1) = yScale; + M(2,2) = zScale; + M(3,3) = 1.0f; + +#undef M + + return(matrix4x4); +} + +static inline TQ3Matrix4x4* Q3Matrix4x4_SetTranslate( + TQ3Matrix4x4 *matrix4x4, + float xTrans, + float yTrans, + float zTrans) +{ + __Q3Memory_Clear(matrix4x4, sizeof(TQ3Matrix4x4)); + +#define M(x,y) matrix4x4->value[x][y] + + M(0,0) = 1.0f; + + M(1,1) = 1.0f; + + M(2,2) = 1.0f; + + M(3,0) = xTrans; + M(3,1) = yTrans; + M(3,2) = zTrans; + M(3,3) = 1.0f; + +#undef M + + return(matrix4x4); +} + +static inline TQ3Matrix4x4* Q3Matrix4x4_Multiply( + const TQ3Matrix4x4 *m1, + const TQ3Matrix4x4 *m2, + TQ3Matrix4x4 *result) +{ + // If result is alias of input, output to temporary + TQ3Matrix4x4 temp; + TQ3Matrix4x4* output = (result == m1 || result == m2 ? &temp : result); + +#define A(x,y) m1->value[x][y] +#define B(x,y) m2->value[x][y] +#define M(x,y) output->value[x][y] + + M(0,0) = A(0,0)*B(0,0) + A(0,1)*B(1,0) + A(0,2)*B(2,0) + A(0,3)*B(3,0); + M(0,1) = A(0,0)*B(0,1) + A(0,1)*B(1,1) + A(0,2)*B(2,1) + A(0,3)*B(3,1); + M(0,2) = A(0,0)*B(0,2) + A(0,1)*B(1,2) + A(0,2)*B(2,2) + A(0,3)*B(3,2); + M(0,3) = A(0,0)*B(0,3) + A(0,1)*B(1,3) + A(0,2)*B(2,3) + A(0,3)*B(3,3); + + M(1,0) = A(1,0)*B(0,0) + A(1,1)*B(1,0) + A(1,2)*B(2,0) + A(1,3)*B(3,0); + M(1,1) = A(1,0)*B(0,1) + A(1,1)*B(1,1) + A(1,2)*B(2,1) + A(1,3)*B(3,1); + M(1,2) = A(1,0)*B(0,2) + A(1,1)*B(1,2) + A(1,2)*B(2,2) + A(1,3)*B(3,2); + M(1,3) = A(1,0)*B(0,3) + A(1,1)*B(1,3) + A(1,2)*B(2,3) + A(1,3)*B(3,3); + + M(2,0) = A(2,0)*B(0,0) + A(2,1)*B(1,0) + A(2,2)*B(2,0) + A(2,3)*B(3,0); + M(2,1) = A(2,0)*B(0,1) + A(2,1)*B(1,1) + A(2,2)*B(2,1) + A(2,3)*B(3,1); + M(2,2) = A(2,0)*B(0,2) + A(2,1)*B(1,2) + A(2,2)*B(2,2) + A(2,3)*B(3,2); + M(2,3) = A(2,0)*B(0,3) + A(2,1)*B(1,3) + A(2,2)*B(2,3) + A(2,3)*B(3,3); + + M(3,0) = A(3,0)*B(0,0) + A(3,1)*B(1,0) + A(3,2)*B(2,0) + A(3,3)*B(3,0); + M(3,1) = A(3,0)*B(0,1) + A(3,1)*B(1,1) + A(3,2)*B(2,1) + A(3,3)*B(3,1); + M(3,2) = A(3,0)*B(0,2) + A(3,1)*B(1,2) + A(3,2)*B(2,2) + A(3,3)*B(3,2); + M(3,3) = A(3,0)*B(0,3) + A(3,1)*B(1,3) + A(3,2)*B(2,3) + A(3,3)*B(3,3); + +#undef A +#undef B +#undef M + + if (output == &temp) + *result = temp; + + return(result); +} + +static inline TQ3Matrix4x4* Q3Matrix4x4_SetRotate_X( + TQ3Matrix4x4 *matrix4x4, + float angle) +{ + float cosAngle = cosf(angle); + float sinAngle = sinf(angle); + + __Q3Memory_Clear(matrix4x4, sizeof(TQ3Matrix4x4)); + +#define M(x,y) matrix4x4->value[x][y] + + M(0,0) = 1.0f; + + M(1,1) = cosAngle; + M(1,2) = sinAngle; + + M(2,1) = -sinAngle; + M(2,2) = cosAngle; + + M(3,3) = 1.0f; + +#undef M + + return(matrix4x4); +} + +static inline TQ3Matrix4x4* Q3Matrix4x4_SetRotate_Y( + TQ3Matrix4x4 *matrix4x4, + float angle) +{ + float cosAngle = cosf(angle); + float sinAngle = sinf(angle); + + __Q3Memory_Clear(matrix4x4, sizeof(TQ3Matrix4x4)); + +#define M(x,y) matrix4x4->value[x][y] + + M(0,0) = cosAngle; + M(0,2) = -sinAngle; + + M(1,1) = 1.0f; + + M(2,0) = sinAngle; + M(2,2) = cosAngle; + + M(3,3) = 1.0f; + +#undef M + + return(matrix4x4); +} + +static inline TQ3Matrix4x4* Q3Matrix4x4_SetRotate_Z( + TQ3Matrix4x4 *matrix4x4, + float angle) +{ + float cosAngle = cosf(angle); + float sinAngle = sinf(angle); + + __Q3Memory_Clear(matrix4x4, sizeof(TQ3Matrix4x4)); + +#define M(x,y) matrix4x4->value[x][y] + + M(0,0) = cosAngle; + M(0,1) = sinAngle; + + M(1,0) = -sinAngle; + M(1,1) = cosAngle; + + M(2,2) = 1.0f; + + M(3,3) = 1.0f; + +#undef M + + return(matrix4x4); +} + +static inline TQ3Matrix4x4* Q3Matrix4x4_SetRotate_XYZ( + TQ3Matrix4x4 *matrix4x4, + float xAngle, + float yAngle, + float zAngle) +{ + float cosX = cosf(xAngle); + float sinX = sinf(xAngle); + float cosY = cosf(yAngle); + float sinY = sinf(yAngle); + float cosZ = cosf(zAngle); + float sinZ = sinf(zAngle); + + float sinXsinY = sinX*sinY; + float cosXsinY = cosX*sinY; + +#define M(x,y) matrix4x4->value[x][y] + + M(0,0) = cosY*cosZ; + M(0,1) = cosY*sinZ; + M(0,2) = -sinY; + M(0,3) = 0.0f; + + M(1,0) = sinXsinY*cosZ - cosX*sinZ; + M(1,1) = sinXsinY*sinZ + cosX*cosZ; + M(1,2) = sinX*cosY; + M(1,3) = 0.0f; + + M(2,0) = cosXsinY*cosZ + sinX*sinZ; + M(2,1) = cosXsinY*sinZ - sinX*cosZ; + M(2,2) = cosX*cosY; + M(2,3) = 0.0f; + + M(3,0) = 0.0f; + M(3,1) = 0.0f; + M(3,2) = 0.0f; + M(3,3) = 1.0f; + +#undef M + + return(matrix4x4); +} + +TQ3Matrix4x4* Q3Matrix4x4_Transpose( + const TQ3Matrix4x4 *matrix4x4, + TQ3Matrix4x4 *result); + +TQ3Matrix4x4* Q3Matrix4x4_Invert( + const TQ3Matrix4x4 *inMatrix, + TQ3Matrix4x4 *result); + +#ifdef __cplusplus +} +#endif