From bd57f345ed9dbed1d81683e48199626de2ea9044 Mon Sep 17 00:00:00 2001 From: 3gg <3gg@shellblade.net> Date: Fri, 27 Jun 2025 10:18:39 -0700 Subject: Restructure project --- contrib/cgltf-tangents/CMakeLists.txt | 13 + contrib/cgltf-tangents/LICENSE | 79 + contrib/cgltf-tangents/MikkTSpace/README.md | 4 + contrib/cgltf-tangents/MikkTSpace/mikktspace.c | 1899 ++++++++++++++++++++++++ contrib/cgltf-tangents/MikkTSpace/mikktspace.h | 145 ++ contrib/cgltf-tangents/README.md | 42 + contrib/cgltf-tangents/cgltf_tangents.c | 618 ++++++++ contrib/cgltf-tangents/cgltf_tangents.h | 67 + contrib/cgltf-tangents/test/CMakeLists.txt | 11 + contrib/cgltf-tangents/test/main.c | 86 ++ 10 files changed, 2964 insertions(+) create mode 100644 contrib/cgltf-tangents/CMakeLists.txt create mode 100644 contrib/cgltf-tangents/LICENSE create mode 100644 contrib/cgltf-tangents/MikkTSpace/README.md create mode 100644 contrib/cgltf-tangents/MikkTSpace/mikktspace.c create mode 100644 contrib/cgltf-tangents/MikkTSpace/mikktspace.h create mode 100644 contrib/cgltf-tangents/README.md create mode 100644 contrib/cgltf-tangents/cgltf_tangents.c create mode 100644 contrib/cgltf-tangents/cgltf_tangents.h create mode 100644 contrib/cgltf-tangents/test/CMakeLists.txt create mode 100644 contrib/cgltf-tangents/test/main.c (limited to 'contrib/cgltf-tangents') diff --git a/contrib/cgltf-tangents/CMakeLists.txt b/contrib/cgltf-tangents/CMakeLists.txt new file mode 100644 index 0000000..2c0771e --- /dev/null +++ b/contrib/cgltf-tangents/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.0) + +project(cgltf-tangents) + +add_library(cgltf-tangents + cgltf_tangents.c + MikkTSpace/mikktspace.c) + +target_include_directories(cgltf-tangents PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}) + +target_link_libraries(cgltf-tangents PUBLIC + cgltf) diff --git a/contrib/cgltf-tangents/LICENSE b/contrib/cgltf-tangents/LICENSE new file mode 100644 index 0000000..7796e37 --- /dev/null +++ b/contrib/cgltf-tangents/LICENSE @@ -0,0 +1,79 @@ +This project has two third-party dependencies: +- MikkTSpace +- cgltf + +The license for this project and its dependencies are included below. + +-------------------------------------------------------------------------------- +cgltf-tangents +-------------------------------------------------------------------------------- + +Copyright 2022 Marc Sunet + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. 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. + +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 HOLDER 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. + +-------------------------------------------------------------------------------- +MikkTSpace +-------------------------------------------------------------------------------- + +Copyright (C) 2011 by Morten S. Mikkelsen + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + +3. This notice may not be removed or altered from any source distribution. + +-------------------------------------------------------------------------------- +cgltf +-------------------------------------------------------------------------------- + +Copyright (c) 2018-2021 Johannes Kuhlmann + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/contrib/cgltf-tangents/MikkTSpace/README.md b/contrib/cgltf-tangents/MikkTSpace/README.md new file mode 100644 index 0000000..9fda155 --- /dev/null +++ b/contrib/cgltf-tangents/MikkTSpace/README.md @@ -0,0 +1,4 @@ +# MikkTSpace +A common standard for tangent space used in baking tools to produce normal maps. + +More information can be found at http://www.mikktspace.com/. diff --git a/contrib/cgltf-tangents/MikkTSpace/mikktspace.c b/contrib/cgltf-tangents/MikkTSpace/mikktspace.c new file mode 100644 index 0000000..0342ae0 --- /dev/null +++ b/contrib/cgltf-tangents/MikkTSpace/mikktspace.c @@ -0,0 +1,1899 @@ +/** \file mikktspace/mikktspace.c + * \ingroup mikktspace + */ +/** + * Copyright (C) 2011 by Morten S. Mikkelsen + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#include +#include +#include +#include +#include +#include + +#include "mikktspace.h" + +#define TFALSE 0 +#define TTRUE 1 + +#ifndef M_PI +#define M_PI 3.1415926535897932384626433832795 +#endif + +#define INTERNAL_RND_SORT_SEED 39871946 + +// internal structure +typedef struct { + float x, y, z; +} SVec3; + +static tbool veq( const SVec3 v1, const SVec3 v2 ) +{ + return (v1.x == v2.x) && (v1.y == v2.y) && (v1.z == v2.z); +} + +static SVec3 vadd( const SVec3 v1, const SVec3 v2 ) +{ + SVec3 vRes; + + vRes.x = v1.x + v2.x; + vRes.y = v1.y + v2.y; + vRes.z = v1.z + v2.z; + + return vRes; +} + + +static SVec3 vsub( const SVec3 v1, const SVec3 v2 ) +{ + SVec3 vRes; + + vRes.x = v1.x - v2.x; + vRes.y = v1.y - v2.y; + vRes.z = v1.z - v2.z; + + return vRes; +} + +static SVec3 vscale(const float fS, const SVec3 v) +{ + SVec3 vRes; + + vRes.x = fS * v.x; + vRes.y = fS * v.y; + vRes.z = fS * v.z; + + return vRes; +} + +static float LengthSquared( const SVec3 v ) +{ + return v.x*v.x + v.y*v.y + v.z*v.z; +} + +static float Length( const SVec3 v ) +{ + return sqrtf(LengthSquared(v)); +} + +static SVec3 Normalize( const SVec3 v ) +{ + return vscale(1 / Length(v), v); +} + +static float vdot( const SVec3 v1, const SVec3 v2) +{ + return v1.x*v2.x + v1.y*v2.y + v1.z*v2.z; +} + + +static tbool NotZero(const float fX) +{ + // could possibly use FLT_EPSILON instead + return fabsf(fX) > FLT_MIN; +} + +static tbool VNotZero(const SVec3 v) +{ + // might change this to an epsilon based test + return NotZero(v.x) || NotZero(v.y) || NotZero(v.z); +} + + + +typedef struct { + int iNrFaces; + int * pTriMembers; +} SSubGroup; + +typedef struct { + int iNrFaces; + int * pFaceIndices; + int iVertexRepresentitive; + tbool bOrientPreservering; +} SGroup; + +// +#define MARK_DEGENERATE 1 +#define QUAD_ONE_DEGEN_TRI 2 +#define GROUP_WITH_ANY 4 +#define ORIENT_PRESERVING 8 + + + +typedef struct { + int FaceNeighbors[3]; + SGroup * AssignedGroup[3]; + + // normalized first order face derivatives + SVec3 vOs, vOt; + float fMagS, fMagT; // original magnitudes + + // determines if the current and the next triangle are a quad. + int iOrgFaceNumber; + int iFlag, iTSpacesOffs; + unsigned char vert_num[4]; +} STriInfo; + +typedef struct { + SVec3 vOs; + float fMagS; + SVec3 vOt; + float fMagT; + int iCounter; // this is to average back into quads. + tbool bOrient; +} STSpace; + +static int GenerateInitialVerticesIndexList(STriInfo pTriInfos[], int piTriList_out[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn); +static void GenerateSharedVerticesIndexList(int piTriList_in_and_out[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn); +static void InitTriInfo(STriInfo pTriInfos[], const int piTriListIn[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn); +static int Build4RuleGroups(STriInfo pTriInfos[], SGroup pGroups[], int piGroupTrianglesBuffer[], const int piTriListIn[], const int iNrTrianglesIn); +static tbool GenerateTSpaces(STSpace psTspace[], const STriInfo pTriInfos[], const SGroup pGroups[], + const int iNrActiveGroups, const int piTriListIn[], const float fThresCos, + const SMikkTSpaceContext * pContext); + +static int MakeIndex(const int iFace, const int iVert) +{ + assert(iVert>=0 && iVert<4 && iFace>=0); + return (iFace<<2) | (iVert&0x3); +} + +static void IndexToData(int * piFace, int * piVert, const int iIndexIn) +{ + piVert[0] = iIndexIn&0x3; + piFace[0] = iIndexIn>>2; +} + +static STSpace AvgTSpace(const STSpace * pTS0, const STSpace * pTS1) +{ + STSpace ts_res; + + // this if is important. Due to floating point precision + // averaging when ts0==ts1 will cause a slight difference + // which results in tangent space splits later on + if (pTS0->fMagS==pTS1->fMagS && pTS0->fMagT==pTS1->fMagT && + veq(pTS0->vOs,pTS1->vOs) && veq(pTS0->vOt, pTS1->vOt)) + { + ts_res.fMagS = pTS0->fMagS; + ts_res.fMagT = pTS0->fMagT; + ts_res.vOs = pTS0->vOs; + ts_res.vOt = pTS0->vOt; + } + else + { + ts_res.fMagS = 0.5f*(pTS0->fMagS+pTS1->fMagS); + ts_res.fMagT = 0.5f*(pTS0->fMagT+pTS1->fMagT); + ts_res.vOs = vadd(pTS0->vOs,pTS1->vOs); + ts_res.vOt = vadd(pTS0->vOt,pTS1->vOt); + if ( VNotZero(ts_res.vOs) ) ts_res.vOs = Normalize(ts_res.vOs); + if ( VNotZero(ts_res.vOt) ) ts_res.vOt = Normalize(ts_res.vOt); + } + + return ts_res; +} + + + +static SVec3 GetPosition(const SMikkTSpaceContext * pContext, const int index); +static SVec3 GetNormal(const SMikkTSpaceContext * pContext, const int index); +static SVec3 GetTexCoord(const SMikkTSpaceContext * pContext, const int index); + + +// degen triangles +static void DegenPrologue(STriInfo pTriInfos[], int piTriList_out[], const int iNrTrianglesIn, const int iTotTris); +static void DegenEpilogue(STSpace psTspace[], STriInfo pTriInfos[], int piTriListIn[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn, const int iTotTris); + + +tbool genTangSpaceDefault(const SMikkTSpaceContext * pContext) +{ + return genTangSpace(pContext, 180.0f); +} + +tbool genTangSpace(const SMikkTSpaceContext * pContext, const float fAngularThreshold) +{ + // count nr_triangles + int * piTriListIn = NULL, * piGroupTrianglesBuffer = NULL; + STriInfo * pTriInfos = NULL; + SGroup * pGroups = NULL; + STSpace * psTspace = NULL; + int iNrTrianglesIn = 0, f=0, t=0, i=0; + int iNrTSPaces = 0, iTotTris = 0, iDegenTriangles = 0, iNrMaxGroups = 0; + int iNrActiveGroups = 0, index = 0; + const int iNrFaces = pContext->m_pInterface->m_getNumFaces(pContext); + tbool bRes = TFALSE; + const float fThresCos = (float) cos((fAngularThreshold*(float)M_PI)/180.0f); + + // verify all call-backs have been set + if ( pContext->m_pInterface->m_getNumFaces==NULL || + pContext->m_pInterface->m_getNumVerticesOfFace==NULL || + pContext->m_pInterface->m_getPosition==NULL || + pContext->m_pInterface->m_getNormal==NULL || + pContext->m_pInterface->m_getTexCoord==NULL ) + return TFALSE; + + // count triangles on supported faces + for (f=0; fm_pInterface->m_getNumVerticesOfFace(pContext, f); + if (verts==3) ++iNrTrianglesIn; + else if (verts==4) iNrTrianglesIn += 2; + } + if (iNrTrianglesIn<=0) return TFALSE; + + // allocate memory for an index list + piTriListIn = (int *) malloc(sizeof(int)*3*iNrTrianglesIn); + pTriInfos = (STriInfo *) malloc(sizeof(STriInfo)*iNrTrianglesIn); + if (piTriListIn==NULL || pTriInfos==NULL) + { + if (piTriListIn!=NULL) free(piTriListIn); + if (pTriInfos!=NULL) free(pTriInfos); + return TFALSE; + } + + // make an initial triangle --> face index list + iNrTSPaces = GenerateInitialVerticesIndexList(pTriInfos, piTriListIn, pContext, iNrTrianglesIn); + + // make a welded index list of identical positions and attributes (pos, norm, texc) + //printf("gen welded index list begin\n"); + GenerateSharedVerticesIndexList(piTriListIn, pContext, iNrTrianglesIn); + //printf("gen welded index list end\n"); + + // Mark all degenerate triangles + iTotTris = iNrTrianglesIn; + iDegenTriangles = 0; + for (t=0; tm_pInterface->m_getNumVerticesOfFace(pContext, f); + if (verts!=3 && verts!=4) continue; + + + // I've decided to let degenerate triangles and group-with-anythings + // vary between left/right hand coordinate systems at the vertices. + // All healthy triangles on the other hand are built to always be either or. + + /*// force the coordinate system orientation to be uniform for every face. + // (this is already the case for good triangles but not for + // degenerate ones and those with bGroupWithAnything==true) + bool bOrient = psTspace[index].bOrient; + if (psTspace[index].iCounter == 0) // tspace was not derived from a group + { + // look for a space created in GenerateTSpaces() by iCounter>0 + bool bNotFound = true; + int i=1; + while (i 0) bNotFound=false; + else ++i; + } + if (!bNotFound) bOrient = psTspace[index+i].bOrient; + }*/ + + // set data + for (i=0; ivOs.x, pTSpace->vOs.y, pTSpace->vOs.z}; + float bitang[] = {pTSpace->vOt.x, pTSpace->vOt.y, pTSpace->vOt.z}; + if (pContext->m_pInterface->m_setTSpace!=NULL) + pContext->m_pInterface->m_setTSpace(pContext, tang, bitang, pTSpace->fMagS, pTSpace->fMagT, pTSpace->bOrient, f, i); + if (pContext->m_pInterface->m_setTSpaceBasic!=NULL) + pContext->m_pInterface->m_setTSpaceBasic(pContext, tang, pTSpace->bOrient==TTRUE ? 1.0f : (-1.0f), f, i); + + ++index; + } + } + + free(psTspace); + + + return TTRUE; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +typedef struct { + float vert[3]; + int index; +} STmpVert; + +static const int g_iCells = 2048; + +#ifdef _MSC_VER +# define NOINLINE __declspec(noinline) +#else +# define NOINLINE __attribute__ ((noinline)) +#endif + +// it is IMPORTANT that this function is called to evaluate the hash since +// inlining could potentially reorder instructions and generate different +// results for the same effective input value fVal. +static NOINLINE int FindGridCell(const float fMin, const float fMax, const float fVal) +{ + const float fIndex = g_iCells * ((fVal-fMin)/(fMax-fMin)); + const int iIndex = (int)fIndex; + return iIndex < g_iCells ? (iIndex >= 0 ? iIndex : 0) : (g_iCells - 1); +} + +static void MergeVertsFast(int piTriList_in_and_out[], STmpVert pTmpVert[], const SMikkTSpaceContext * pContext, const int iL_in, const int iR_in); +static void MergeVertsSlow(int piTriList_in_and_out[], const SMikkTSpaceContext * pContext, const int pTable[], const int iEntries); +static void GenerateSharedVerticesIndexListSlow(int piTriList_in_and_out[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn); + +static void GenerateSharedVerticesIndexList(int piTriList_in_and_out[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn) +{ + + // Generate bounding box + int * piHashTable=NULL, * piHashCount=NULL, * piHashOffsets=NULL, * piHashCount2=NULL; + STmpVert * pTmpVert = NULL; + int i=0, iChannel=0, k=0, e=0; + int iMaxCount=0; + SVec3 vMin = GetPosition(pContext, 0), vMax = vMin, vDim; + float fMin, fMax; + for (i=1; i<(iNrTrianglesIn*3); i++) + { + const int index = piTriList_in_and_out[i]; + + const SVec3 vP = GetPosition(pContext, index); + if (vMin.x > vP.x) vMin.x = vP.x; + else if (vMax.x < vP.x) vMax.x = vP.x; + if (vMin.y > vP.y) vMin.y = vP.y; + else if (vMax.y < vP.y) vMax.y = vP.y; + if (vMin.z > vP.z) vMin.z = vP.z; + else if (vMax.z < vP.z) vMax.z = vP.z; + } + + vDim = vsub(vMax,vMin); + iChannel = 0; + fMin = vMin.x; fMax=vMax.x; + if (vDim.y>vDim.x && vDim.y>vDim.z) + { + iChannel=1; + fMin = vMin.y; + fMax = vMax.y; + } + else if (vDim.z>vDim.x) + { + iChannel=2; + fMin = vMin.z; + fMax = vMax.z; + } + + // make allocations + piHashTable = (int *) malloc(sizeof(int)*iNrTrianglesIn*3); + piHashCount = (int *) malloc(sizeof(int)*g_iCells); + piHashOffsets = (int *) malloc(sizeof(int)*g_iCells); + piHashCount2 = (int *) malloc(sizeof(int)*g_iCells); + + if (piHashTable==NULL || piHashCount==NULL || piHashOffsets==NULL || piHashCount2==NULL) + { + if (piHashTable!=NULL) free(piHashTable); + if (piHashCount!=NULL) free(piHashCount); + if (piHashOffsets!=NULL) free(piHashOffsets); + if (piHashCount2!=NULL) free(piHashCount2); + GenerateSharedVerticesIndexListSlow(piTriList_in_and_out, pContext, iNrTrianglesIn); + return; + } + memset(piHashCount, 0, sizeof(int)*g_iCells); + memset(piHashCount2, 0, sizeof(int)*g_iCells); + + // count amount of elements in each cell unit + for (i=0; i<(iNrTrianglesIn*3); i++) + { + const int index = piTriList_in_and_out[i]; + const SVec3 vP = GetPosition(pContext, index); + const float fVal = iChannel==0 ? vP.x : (iChannel==1 ? vP.y : vP.z); + const int iCell = FindGridCell(fMin, fMax, fVal); + ++piHashCount[iCell]; + } + + // evaluate start index of each cell. + piHashOffsets[0]=0; + for (k=1; kpTmpVert[l].vert[c]) fvMin[c]=pTmpVert[l].vert[c]; + if (fvMax[c]dx && dy>dz) channel=1; + else if (dz>dx) channel=2; + + fSep = 0.5f*(fvMax[channel]+fvMin[channel]); + + // stop if all vertices are NaNs + if (!isfinite(fSep)) + return; + + // terminate recursion when the separation/average value + // is no longer strictly between fMin and fMax values. + if (fSep>=fvMax[channel] || fSep<=fvMin[channel]) + { + // complete the weld + for (l=iL_in; l<=iR_in; l++) + { + int i = pTmpVert[l].index; + const int index = piTriList_in_and_out[i]; + const SVec3 vP = GetPosition(pContext, index); + const SVec3 vN = GetNormal(pContext, index); + const SVec3 vT = GetTexCoord(pContext, index); + + tbool bNotFound = TTRUE; + int l2=iL_in, i2rec=-1; + while (l20); // at least 2 entries + + // separate (by fSep) all points between iL_in and iR_in in pTmpVert[] + while (iL < iR) + { + tbool bReadyLeftSwap = TFALSE, bReadyRightSwap = TFALSE; + while ((!bReadyLeftSwap) && iL=iL_in && iL<=iR_in); + bReadyLeftSwap = !(pTmpVert[iL].vert[channel]=iL_in && iR<=iR_in); + bReadyRightSwap = pTmpVert[iR].vert[channel]m_pInterface->m_getNumFaces(pContext); f++) + { + const int verts = pContext->m_pInterface->m_getNumVerticesOfFace(pContext, f); + if (verts!=3 && verts!=4) continue; + + pTriInfos[iDstTriIndex].iOrgFaceNumber = f; + pTriInfos[iDstTriIndex].iTSpacesOffs = iTSpacesOffs; + + if (verts==3) + { + unsigned char * pVerts = pTriInfos[iDstTriIndex].vert_num; + pVerts[0]=0; pVerts[1]=1; pVerts[2]=2; + piTriList_out[iDstTriIndex*3+0] = MakeIndex(f, 0); + piTriList_out[iDstTriIndex*3+1] = MakeIndex(f, 1); + piTriList_out[iDstTriIndex*3+2] = MakeIndex(f, 2); + ++iDstTriIndex; // next + } + else + { + { + pTriInfos[iDstTriIndex+1].iOrgFaceNumber = f; + pTriInfos[iDstTriIndex+1].iTSpacesOffs = iTSpacesOffs; + } + + { + // need an order independent way to evaluate + // tspace on quads. This is done by splitting + // along the shortest diagonal. + const int i0 = MakeIndex(f, 0); + const int i1 = MakeIndex(f, 1); + const int i2 = MakeIndex(f, 2); + const int i3 = MakeIndex(f, 3); + const SVec3 T0 = GetTexCoord(pContext, i0); + const SVec3 T1 = GetTexCoord(pContext, i1); + const SVec3 T2 = GetTexCoord(pContext, i2); + const SVec3 T3 = GetTexCoord(pContext, i3); + const float distSQ_02 = LengthSquared(vsub(T2,T0)); + const float distSQ_13 = LengthSquared(vsub(T3,T1)); + tbool bQuadDiagIs_02; + if (distSQ_02m_pInterface->m_getPosition(pContext, pos, iF, iI); + res.x=pos[0]; res.y=pos[1]; res.z=pos[2]; + return res; +} + +static SVec3 GetNormal(const SMikkTSpaceContext * pContext, const int index) +{ + int iF, iI; + SVec3 res; float norm[3]; + IndexToData(&iF, &iI, index); + pContext->m_pInterface->m_getNormal(pContext, norm, iF, iI); + res.x=norm[0]; res.y=norm[1]; res.z=norm[2]; + return res; +} + +static SVec3 GetTexCoord(const SMikkTSpaceContext * pContext, const int index) +{ + int iF, iI; + SVec3 res; float texc[2]; + IndexToData(&iF, &iI, index); + pContext->m_pInterface->m_getTexCoord(pContext, texc, iF, iI); + res.x=texc[0]; res.y=texc[1]; res.z=1.0f; + return res; +} + +///////////////////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////////////////// + +typedef union { + struct + { + int i0, i1, f; + }; + int array[3]; +} SEdge; + +static void BuildNeighborsFast(STriInfo pTriInfos[], SEdge * pEdges, const int piTriListIn[], const int iNrTrianglesIn); +static void BuildNeighborsSlow(STriInfo pTriInfos[], const int piTriListIn[], const int iNrTrianglesIn); + +// returns the texture area times 2 +static float CalcTexArea(const SMikkTSpaceContext * pContext, const int indices[]) +{ + const SVec3 t1 = GetTexCoord(pContext, indices[0]); + const SVec3 t2 = GetTexCoord(pContext, indices[1]); + const SVec3 t3 = GetTexCoord(pContext, indices[2]); + + const float t21x = t2.x-t1.x; + const float t21y = t2.y-t1.y; + const float t31x = t3.x-t1.x; + const float t31y = t3.y-t1.y; + + const float fSignedAreaSTx2 = t21x*t31y - t21y*t31x; + + return fSignedAreaSTx2<0 ? (-fSignedAreaSTx2) : fSignedAreaSTx2; +} + +static void InitTriInfo(STriInfo pTriInfos[], const int piTriListIn[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn) +{ + int f=0, i=0, t=0; + // pTriInfos[f].iFlag is cleared in GenerateInitialVerticesIndexList() which is called before this function. + + // generate neighbor info list + for (f=0; f0 ? ORIENT_PRESERVING : 0); + + if ( NotZero(fSignedAreaSTx2) ) + { + const float fAbsArea = fabsf(fSignedAreaSTx2); + const float fLenOs = Length(vOs); + const float fLenOt = Length(vOt); + const float fS = (pTriInfos[f].iFlag&ORIENT_PRESERVING)==0 ? (-1.0f) : 1.0f; + if ( NotZero(fLenOs) ) pTriInfos[f].vOs = vscale(fS/fLenOs, vOs); + if ( NotZero(fLenOt) ) pTriInfos[f].vOt = vscale(fS/fLenOt, vOt); + + // evaluate magnitudes prior to normalization of vOs and vOt + pTriInfos[f].fMagS = fLenOs / fAbsArea; + pTriInfos[f].fMagT = fLenOt / fAbsArea; + + // if this is a good triangle + if ( NotZero(pTriInfos[f].fMagS) && NotZero(pTriInfos[f].fMagT)) + pTriInfos[f].iFlag &= (~GROUP_WITH_ANY); + } + } + + // force otherwise healthy quads to a fixed orientation + while (t<(iNrTrianglesIn-1)) + { + const int iFO_a = pTriInfos[t].iOrgFaceNumber; + const int iFO_b = pTriInfos[t+1].iOrgFaceNumber; + if (iFO_a==iFO_b) // this is a quad + { + const tbool bIsDeg_a = (pTriInfos[t].iFlag&MARK_DEGENERATE)!=0 ? TTRUE : TFALSE; + const tbool bIsDeg_b = (pTriInfos[t+1].iFlag&MARK_DEGENERATE)!=0 ? TTRUE : TFALSE; + + // bad triangles should already have been removed by + // DegenPrologue(), but just in case check bIsDeg_a and bIsDeg_a are false + if ((bIsDeg_a||bIsDeg_b)==TFALSE) + { + const tbool bOrientA = (pTriInfos[t].iFlag&ORIENT_PRESERVING)!=0 ? TTRUE : TFALSE; + const tbool bOrientB = (pTriInfos[t+1].iFlag&ORIENT_PRESERVING)!=0 ? TTRUE : TFALSE; + // if this happens the quad has extremely bad mapping!! + if (bOrientA!=bOrientB) + { + //printf("found quad with bad mapping\n"); + tbool bChooseOrientFirstTri = TFALSE; + if ((pTriInfos[t+1].iFlag&GROUP_WITH_ANY)!=0) bChooseOrientFirstTri = TTRUE; + else if ( CalcTexArea(pContext, &piTriListIn[t*3+0]) >= CalcTexArea(pContext, &piTriListIn[(t+1)*3+0]) ) + bChooseOrientFirstTri = TTRUE; + + // force match + { + const int t0 = bChooseOrientFirstTri ? t : (t+1); + const int t1 = bChooseOrientFirstTri ? (t+1) : t; + pTriInfos[t1].iFlag &= (~ORIENT_PRESERVING); // clear first + pTriInfos[t1].iFlag |= (pTriInfos[t0].iFlag&ORIENT_PRESERVING); // copy bit + } + } + } + t += 2; + } + else + ++t; + } + + // match up edge pairs + { + SEdge * pEdges = (SEdge *) malloc(sizeof(SEdge)*iNrTrianglesIn*3); + if (pEdges==NULL) + BuildNeighborsSlow(pTriInfos, piTriListIn, iNrTrianglesIn); + else + { + BuildNeighborsFast(pTriInfos, pEdges, piTriListIn, iNrTrianglesIn); + + free(pEdges); + } + } +} + +///////////////////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////////////////// + +static tbool AssignRecur(const int piTriListIn[], STriInfo psTriInfos[], const int iMyTriIndex, SGroup * pGroup); +static void AddTriToGroup(SGroup * pGroup, const int iTriIndex); + +static int Build4RuleGroups(STriInfo pTriInfos[], SGroup pGroups[], int piGroupTrianglesBuffer[], const int piTriListIn[], const int iNrTrianglesIn) +{ + const int iNrMaxGroups = iNrTrianglesIn*3; + int iNrActiveGroups = 0; + int iOffset = 0, f=0, i=0; + (void)iNrMaxGroups; /* quiet warnings in non debug mode */ + for (f=0; fiVertexRepresentitive = vert_index; + pTriInfos[f].AssignedGroup[i]->bOrientPreservering = (pTriInfos[f].iFlag&ORIENT_PRESERVING)!=0; + pTriInfos[f].AssignedGroup[i]->iNrFaces = 0; + pTriInfos[f].AssignedGroup[i]->pFaceIndices = &piGroupTrianglesBuffer[iOffset]; + ++iNrActiveGroups; + + AddTriToGroup(pTriInfos[f].AssignedGroup[i], f); + bOrPre = (pTriInfos[f].iFlag&ORIENT_PRESERVING)!=0 ? TTRUE : TFALSE; + neigh_indexL = pTriInfos[f].FaceNeighbors[i]; + neigh_indexR = pTriInfos[f].FaceNeighbors[i>0?(i-1):2]; + if (neigh_indexL>=0) // neighbor + { + const tbool bAnswer = + AssignRecur(piTriListIn, pTriInfos, neigh_indexL, + pTriInfos[f].AssignedGroup[i] ); + + const tbool bOrPre2 = (pTriInfos[neigh_indexL].iFlag&ORIENT_PRESERVING)!=0 ? TTRUE : TFALSE; + const tbool bDiff = bOrPre!=bOrPre2 ? TTRUE : TFALSE; + assert(bAnswer || bDiff); + (void)bAnswer, (void)bDiff; /* quiet warnings in non debug mode */ + } + if (neigh_indexR>=0) // neighbor + { + const tbool bAnswer = + AssignRecur(piTriListIn, pTriInfos, neigh_indexR, + pTriInfos[f].AssignedGroup[i] ); + + const tbool bOrPre2 = (pTriInfos[neigh_indexR].iFlag&ORIENT_PRESERVING)!=0 ? TTRUE : TFALSE; + const tbool bDiff = bOrPre!=bOrPre2 ? TTRUE : TFALSE; + assert(bAnswer || bDiff); + (void)bAnswer, (void)bDiff; /* quiet warnings in non debug mode */ + } + + // update offset + iOffset += pTriInfos[f].AssignedGroup[i]->iNrFaces; + // since the groups are disjoint a triangle can never + // belong to more than 3 groups. Subsequently something + // is completely screwed if this assertion ever hits. + assert(iOffset <= iNrMaxGroups); + } + } + } + + return iNrActiveGroups; +} + +static void AddTriToGroup(SGroup * pGroup, const int iTriIndex) +{ + pGroup->pFaceIndices[pGroup->iNrFaces] = iTriIndex; + ++pGroup->iNrFaces; +} + +static tbool AssignRecur(const int piTriListIn[], STriInfo psTriInfos[], + const int iMyTriIndex, SGroup * pGroup) +{ + STriInfo * pMyTriInfo = &psTriInfos[iMyTriIndex]; + + // track down vertex + const int iVertRep = pGroup->iVertexRepresentitive; + const int * pVerts = &piTriListIn[3*iMyTriIndex+0]; + int i=-1; + if (pVerts[0]==iVertRep) i=0; + else if (pVerts[1]==iVertRep) i=1; + else if (pVerts[2]==iVertRep) i=2; + assert(i>=0 && i<3); + + // early out + if (pMyTriInfo->AssignedGroup[i] == pGroup) return TTRUE; + else if (pMyTriInfo->AssignedGroup[i]!=NULL) return TFALSE; + if ((pMyTriInfo->iFlag&GROUP_WITH_ANY)!=0) + { + // first to group with a group-with-anything triangle + // determines it's orientation. + // This is the only existing order dependency in the code!! + if ( pMyTriInfo->AssignedGroup[0] == NULL && + pMyTriInfo->AssignedGroup[1] == NULL && + pMyTriInfo->AssignedGroup[2] == NULL ) + { + pMyTriInfo->iFlag &= (~ORIENT_PRESERVING); + pMyTriInfo->iFlag |= (pGroup->bOrientPreservering ? ORIENT_PRESERVING : 0); + } + } + { + const tbool bOrient = (pMyTriInfo->iFlag&ORIENT_PRESERVING)!=0 ? TTRUE : TFALSE; + if (bOrient != pGroup->bOrientPreservering) return TFALSE; + } + + AddTriToGroup(pGroup, iMyTriIndex); + pMyTriInfo->AssignedGroup[i] = pGroup; + + { + const int neigh_indexL = pMyTriInfo->FaceNeighbors[i]; + const int neigh_indexR = pMyTriInfo->FaceNeighbors[i>0?(i-1):2]; + if (neigh_indexL>=0) + AssignRecur(piTriListIn, psTriInfos, neigh_indexL, pGroup); + if (neigh_indexR>=0) + AssignRecur(piTriListIn, psTriInfos, neigh_indexR, pGroup); + } + + + + return TTRUE; +} + +///////////////////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////////////////// + +static tbool CompareSubGroups(const SSubGroup * pg1, const SSubGroup * pg2); +static void QuickSort(int* pSortBuffer, int iLeft, int iRight, unsigned int uSeed); +static STSpace EvalTspace(int face_indices[], const int iFaces, const int piTriListIn[], const STriInfo pTriInfos[], const SMikkTSpaceContext * pContext, const int iVertexRepresentitive); + +static tbool GenerateTSpaces(STSpace psTspace[], const STriInfo pTriInfos[], const SGroup pGroups[], + const int iNrActiveGroups, const int piTriListIn[], const float fThresCos, + const SMikkTSpaceContext * pContext) +{ + STSpace * pSubGroupTspace = NULL; + SSubGroup * pUniSubGroups = NULL; + int * pTmpMembers = NULL; + int iMaxNrFaces=0, iUniqueTspaces=0, g=0, i=0; + for (g=0; giNrFaces; i++) // triangles + { + const int f = pGroup->pFaceIndices[i]; // triangle number + int index=-1, iVertIndex=-1, iOF_1=-1, iMembers=0, j=0, l=0; + SSubGroup tmp_group; + tbool bFound; + SVec3 n, vOs, vOt; + if (pTriInfos[f].AssignedGroup[0]==pGroup) index=0; + else if (pTriInfos[f].AssignedGroup[1]==pGroup) index=1; + else if (pTriInfos[f].AssignedGroup[2]==pGroup) index=2; + assert(index>=0 && index<3); + + iVertIndex = piTriListIn[f*3+index]; + assert(iVertIndex==pGroup->iVertexRepresentitive); + + // is normalized already + n = GetNormal(pContext, iVertIndex); + + // project + vOs = vsub(pTriInfos[f].vOs, vscale(vdot(n,pTriInfos[f].vOs), n)); + vOt = vsub(pTriInfos[f].vOt, vscale(vdot(n,pTriInfos[f].vOt), n)); + if ( VNotZero(vOs) ) vOs = Normalize(vOs); + if ( VNotZero(vOt) ) vOt = Normalize(vOt); + + // original face number + iOF_1 = pTriInfos[f].iOrgFaceNumber; + + iMembers = 0; + for (j=0; jiNrFaces; j++) + { + const int t = pGroup->pFaceIndices[j]; // triangle number + const int iOF_2 = pTriInfos[t].iOrgFaceNumber; + + // project + SVec3 vOs2 = vsub(pTriInfos[t].vOs, vscale(vdot(n,pTriInfos[t].vOs), n)); + SVec3 vOt2 = vsub(pTriInfos[t].vOt, vscale(vdot(n,pTriInfos[t].vOt), n)); + if ( VNotZero(vOs2) ) vOs2 = Normalize(vOs2); + if ( VNotZero(vOt2) ) vOt2 = Normalize(vOt2); + + { + const tbool bAny = ( (pTriInfos[f].iFlag | pTriInfos[t].iFlag) & GROUP_WITH_ANY )!=0 ? TTRUE : TFALSE; + // make sure triangles which belong to the same quad are joined. + const tbool bSameOrgFace = iOF_1==iOF_2 ? TTRUE : TFALSE; + + const float fCosS = vdot(vOs,vOs2); + const float fCosT = vdot(vOt,vOt2); + + assert(f!=t || bSameOrgFace); // sanity check + if (bAny || bSameOrgFace || (fCosS>fThresCos && fCosT>fThresCos)) + pTmpMembers[iMembers++] = t; + } + } + + // sort pTmpMembers + tmp_group.iNrFaces = iMembers; + tmp_group.pTriMembers = pTmpMembers; + if (iMembers>1) + { + unsigned int uSeed = INTERNAL_RND_SORT_SEED; // could replace with a random seed? + QuickSort(pTmpMembers, 0, iMembers-1, uSeed); + } + + // look for an existing match + bFound = TFALSE; + l=0; + while (liVertexRepresentitive); + ++iUniqueSubGroups; + } + + // output tspace + { + const int iOffs = pTriInfos[f].iTSpacesOffs; + const int iVert = pTriInfos[f].vert_num[index]; + STSpace * pTS_out = &psTspace[iOffs+iVert]; + assert(pTS_out->iCounter<2); + assert(((pTriInfos[f].iFlag&ORIENT_PRESERVING)!=0) == pGroup->bOrientPreservering); + if (pTS_out->iCounter==1) + { + *pTS_out = AvgTSpace(pTS_out, &pSubGroupTspace[l]); + pTS_out->iCounter = 2; // update counter + pTS_out->bOrient = pGroup->bOrientPreservering; + } + else + { + assert(pTS_out->iCounter==0); + *pTS_out = pSubGroupTspace[l]; + pTS_out->iCounter = 1; // update counter + pTS_out->bOrient = pGroup->bOrientPreservering; + } + } + } + + // clean up and offset iUniqueTspaces + for (s=0; s=0 && i<3); + + // project + index = piTriListIn[3*f+i]; + n = GetNormal(pContext, index); + vOs = vsub(pTriInfos[f].vOs, vscale(vdot(n,pTriInfos[f].vOs), n)); + vOt = vsub(pTriInfos[f].vOt, vscale(vdot(n,pTriInfos[f].vOt), n)); + if ( VNotZero(vOs) ) vOs = Normalize(vOs); + if ( VNotZero(vOt) ) vOt = Normalize(vOt); + + i2 = piTriListIn[3*f + (i<2?(i+1):0)]; + i1 = piTriListIn[3*f + i]; + i0 = piTriListIn[3*f + (i>0?(i-1):2)]; + + p0 = GetPosition(pContext, i0); + p1 = GetPosition(pContext, i1); + p2 = GetPosition(pContext, i2); + v1 = vsub(p0,p1); + v2 = vsub(p2,p1); + + // project + v1 = vsub(v1, vscale(vdot(n,v1),n)); if ( VNotZero(v1) ) v1 = Normalize(v1); + v2 = vsub(v2, vscale(vdot(n,v2),n)); if ( VNotZero(v2) ) v2 = Normalize(v2); + + // weight contribution by the angle + // between the two edge vectors + fCos = vdot(v1,v2); fCos=fCos>1?1:(fCos<(-1) ? (-1) : fCos); + fAngle = (float) acos(fCos); + fMagS = pTriInfos[f].fMagS; + fMagT = pTriInfos[f].fMagT; + + res.vOs=vadd(res.vOs, vscale(fAngle,vOs)); + res.vOt=vadd(res.vOt,vscale(fAngle,vOt)); + res.fMagS+=(fAngle*fMagS); + res.fMagT+=(fAngle*fMagT); + fAngleSum += fAngle; + } + } + + // normalize + if ( VNotZero(res.vOs) ) res.vOs = Normalize(res.vOs); + if ( VNotZero(res.vOt) ) res.vOt = Normalize(res.vOt); + if (fAngleSum>0) + { + res.fMagS /= fAngleSum; + res.fMagT /= fAngleSum; + } + + return res; +} + +static tbool CompareSubGroups(const SSubGroup * pg1, const SSubGroup * pg2) +{ + tbool bStillSame=TTRUE; + int i=0; + if (pg1->iNrFaces!=pg2->iNrFaces) return TFALSE; + while (iiNrFaces && bStillSame) + { + bStillSame = pg1->pTriMembers[i]==pg2->pTriMembers[i] ? TTRUE : TFALSE; + if (bStillSame) ++i; + } + return bStillSame; +} + +static void QuickSort(int* pSortBuffer, int iLeft, int iRight, unsigned int uSeed) +{ + int iL, iR, n, index, iMid, iTmp; + + // Random + unsigned int t=uSeed&31; + t=(uSeed<>(32-t)); + uSeed=uSeed+t+3; + // Random end + + iL=iLeft; iR=iRight; + n = (iR-iL)+1; + assert(n>=0); + index = (int) (uSeed%n); + + iMid=pSortBuffer[index + iL]; + + + do + { + while (pSortBuffer[iL] < iMid) + ++iL; + while (pSortBuffer[iR] > iMid) + --iR; + + if (iL <= iR) + { + iTmp = pSortBuffer[iL]; + pSortBuffer[iL] = pSortBuffer[iR]; + pSortBuffer[iR] = iTmp; + ++iL; --iR; + } + } + while (iL <= iR); + + if (iLeft < iR) + QuickSort(pSortBuffer, iLeft, iR, uSeed); + if (iL < iRight) + QuickSort(pSortBuffer, iL, iRight, uSeed); +} + +///////////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////////// + +static void QuickSortEdges(SEdge * pSortBuffer, int iLeft, int iRight, const int channel, unsigned int uSeed); +static void GetEdge(int * i0_out, int * i1_out, int * edgenum_out, const int indices[], const int i0_in, const int i1_in); + +static void BuildNeighborsFast(STriInfo pTriInfos[], SEdge * pEdges, const int piTriListIn[], const int iNrTrianglesIn) +{ + // build array of edges + unsigned int uSeed = INTERNAL_RND_SORT_SEED; // could replace with a random seed? + int iEntries=0, iCurStartIndex=-1, f=0, i=0; + for (f=0; f pSortBuffer[iRight].array[channel]) + { + sTmp = pSortBuffer[iLeft]; + pSortBuffer[iLeft] = pSortBuffer[iRight]; + pSortBuffer[iRight] = sTmp; + } + return; + } + + // Random + t=uSeed&31; + t=(uSeed<>(32-t)); + uSeed=uSeed+t+3; + // Random end + + iL = iLeft; + iR = iRight; + n = (iR-iL)+1; + assert(n>=0); + index = (int) (uSeed%n); + + iMid=pSortBuffer[index + iL].array[channel]; + + do + { + while (pSortBuffer[iL].array[channel] < iMid) + ++iL; + while (pSortBuffer[iR].array[channel] > iMid) + --iR; + + if (iL <= iR) + { + sTmp = pSortBuffer[iL]; + pSortBuffer[iL] = pSortBuffer[iR]; + pSortBuffer[iR] = sTmp; + ++iL; --iR; + } + } + while (iL <= iR); + + if (iLeft < iR) + QuickSortEdges(pSortBuffer, iLeft, iR, channel, uSeed); + if (iL < iRight) + QuickSortEdges(pSortBuffer, iL, iRight, channel, uSeed); +} + +// resolve ordering and edge number +static void GetEdge(int * i0_out, int * i1_out, int * edgenum_out, const int indices[], const int i0_in, const int i1_in) +{ + *edgenum_out = -1; + + // test if first index is on the edge + if (indices[0]==i0_in || indices[0]==i1_in) + { + // test if second index is on the edge + if (indices[1]==i0_in || indices[1]==i1_in) + { + edgenum_out[0]=0; // first edge + i0_out[0]=indices[0]; + i1_out[0]=indices[1]; + } + else + { + edgenum_out[0]=2; // third edge + i0_out[0]=indices[2]; + i1_out[0]=indices[0]; + } + } + else + { + // only second and third index is on the edge + edgenum_out[0]=1; // second edge + i0_out[0]=indices[1]; + i1_out[0]=indices[2]; + } +} + + +///////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////// Degenerate triangles //////////////////////////////////// + +static void DegenPrologue(STriInfo pTriInfos[], int piTriList_out[], const int iNrTrianglesIn, const int iTotTris) +{ + int iNextGoodTriangleSearchIndex=-1; + tbool bStillFindingGoodOnes; + + // locate quads with only one good triangle + int t=0; + while (t<(iTotTris-1)) + { + const int iFO_a = pTriInfos[t].iOrgFaceNumber; + const int iFO_b = pTriInfos[t+1].iOrgFaceNumber; + if (iFO_a==iFO_b) // this is a quad + { + const tbool bIsDeg_a = (pTriInfos[t].iFlag&MARK_DEGENERATE)!=0 ? TTRUE : TFALSE; + const tbool bIsDeg_b = (pTriInfos[t+1].iFlag&MARK_DEGENERATE)!=0 ? TTRUE : TFALSE; + if ((bIsDeg_a^bIsDeg_b)!=0) + { + pTriInfos[t].iFlag |= QUAD_ONE_DEGEN_TRI; + pTriInfos[t+1].iFlag |= QUAD_ONE_DEGEN_TRI; + } + t += 2; + } + else + ++t; + } + + // reorder list so all degen triangles are moved to the back + // without reordering the good triangles + iNextGoodTriangleSearchIndex = 1; + t=0; + bStillFindingGoodOnes = TTRUE; + while (t (t+1)); + + // swap triangle t0 and t1 + if (!bJustADegenerate) + { + int i=0; + for (i=0; i<3; i++) + { + const int index = piTriList_out[t0*3+i]; + piTriList_out[t0*3+i] = piTriList_out[t1*3+i]; + piTriList_out[t1*3+i] = index; + } + { + const STriInfo tri_info = pTriInfos[t0]; + pTriInfos[t0] = pTriInfos[t1]; + pTriInfos[t1] = tri_info; + } + } + else + bStillFindingGoodOnes = TFALSE; // this is not supposed to happen + } + + if (bStillFindingGoodOnes) ++t; + } + + assert(bStillFindingGoodOnes); // code will still work. + assert(iNrTrianglesIn == t); +} + +static void DegenEpilogue(STSpace psTspace[], STriInfo pTriInfos[], int piTriListIn[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn, const int iTotTris) +{ + int t=0, i=0; + // deal with degenerate triangles + // punishment for degenerate triangles is O(N^2) + for (t=iNrTrianglesIn; t http://image.diku.dk/projects/media/morten.mikkelsen.08.pdf + * Note that though the tangent spaces at the vertices are generated in an order-independent way, + * by this implementation, the interpolated tangent space is still affected by which diagonal is + * chosen to split each quad. A sensible solution is to have your tools pipeline always + * split quads by the shortest diagonal. This choice is order-independent and works with mirroring. + * If these have the same length then compare the diagonals defined by the texture coordinates. + * XNormal which is a tool for baking normal maps allows you to write your own tangent space plugin + * and also quad triangulator plugin. + */ + + +typedef int tbool; +typedef struct SMikkTSpaceContext SMikkTSpaceContext; + +typedef struct { + // Returns the number of faces (triangles/quads) on the mesh to be processed. + int (*m_getNumFaces)(const SMikkTSpaceContext * pContext); + + // Returns the number of vertices on face number iFace + // iFace is a number in the range {0, 1, ..., getNumFaces()-1} + int (*m_getNumVerticesOfFace)(const SMikkTSpaceContext * pContext, const int iFace); + + // returns the position/normal/texcoord of the referenced face of vertex number iVert. + // iVert is in the range {0,1,2} for triangles and {0,1,2,3} for quads. + void (*m_getPosition)(const SMikkTSpaceContext * pContext, float fvPosOut[], const int iFace, const int iVert); + void (*m_getNormal)(const SMikkTSpaceContext * pContext, float fvNormOut[], const int iFace, const int iVert); + void (*m_getTexCoord)(const SMikkTSpaceContext * pContext, float fvTexcOut[], const int iFace, const int iVert); + + // either (or both) of the two setTSpace callbacks can be set. + // The call-back m_setTSpaceBasic() is sufficient for basic normal mapping. + + // This function is used to return the tangent and fSign to the application. + // fvTangent is a unit length vector. + // For normal maps it is sufficient to use the following simplified version of the bitangent which is generated at pixel/vertex level. + // bitangent = fSign * cross(vN, tangent); + // Note that the results are returned unindexed. It is possible to generate a new index list + // But averaging/overwriting tangent spaces by using an already existing index list WILL produce INCRORRECT results. + // DO NOT! use an already existing index list. + void (*m_setTSpaceBasic)(const SMikkTSpaceContext * pContext, const float fvTangent[], const float fSign, const int iFace, const int iVert); + + // This function is used to return tangent space results to the application. + // fvTangent and fvBiTangent are unit length vectors and fMagS and fMagT are their + // true magnitudes which can be used for relief mapping effects. + // fvBiTangent is the "real" bitangent and thus may not be perpendicular to fvTangent. + // However, both are perpendicular to the vertex normal. + // For normal maps it is sufficient to use the following simplified version of the bitangent which is generated at pixel/vertex level. + // fSign = bIsOrientationPreserving ? 1.0f : (-1.0f); + // bitangent = fSign * cross(vN, tangent); + // Note that the results are returned unindexed. It is possible to generate a new index list + // But averaging/overwriting tangent spaces by using an already existing index list WILL produce INCRORRECT results. + // DO NOT! use an already existing index list. + void (*m_setTSpace)(const SMikkTSpaceContext * pContext, const float fvTangent[], const float fvBiTangent[], const float fMagS, const float fMagT, + const tbool bIsOrientationPreserving, const int iFace, const int iVert); +} SMikkTSpaceInterface; + +struct SMikkTSpaceContext +{ + SMikkTSpaceInterface * m_pInterface; // initialized with callback functions + void * m_pUserData; // pointer to client side mesh data etc. (passed as the first parameter with every interface call) +}; + +// these are both thread safe! +tbool genTangSpaceDefault(const SMikkTSpaceContext * pContext); // Default (recommended) fAngularThreshold is 180 degrees (which means threshold disabled) +tbool genTangSpace(const SMikkTSpaceContext * pContext, const float fAngularThreshold); + + +// To avoid visual errors (distortions/unwanted hard edges in lighting), when using sampled normal maps, the +// normal map sampler must use the exact inverse of the pixel shader transformation. +// The most efficient transformation we can possibly do in the pixel shader is +// achieved by using, directly, the "unnormalized" interpolated tangent, bitangent and vertex normal: vT, vB and vN. +// pixel shader (fast transform out) +// vNout = normalize( vNt.x * vT + vNt.y * vB + vNt.z * vN ); +// where vNt is the tangent space normal. The normal map sampler must likewise use the +// interpolated and "unnormalized" tangent, bitangent and vertex normal to be compliant with the pixel shader. +// sampler does (exact inverse of pixel shader): +// float3 row0 = cross(vB, vN); +// float3 row1 = cross(vN, vT); +// float3 row2 = cross(vT, vB); +// float fSign = dot(vT, row0)<0 ? -1 : 1; +// vNt = normalize( fSign * float3(dot(vNout,row0), dot(vNout,row1), dot(vNout,row2)) ); +// where vNout is the sampled normal in some chosen 3D space. +// +// Should you choose to reconstruct the bitangent in the pixel shader instead +// of the vertex shader, as explained earlier, then be sure to do this in the normal map sampler also. +// Finally, beware of quad triangulations. If the normal map sampler doesn't use the same triangulation of +// quads as your renderer then problems will occur since the interpolated tangent spaces will differ +// eventhough the vertex level tangent spaces match. This can be solved either by triangulating before +// sampling/exporting or by using the order-independent choice of diagonal for splitting quads suggested earlier. +// However, this must be used both by the sampler and your tools/rendering pipeline. + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/contrib/cgltf-tangents/README.md b/contrib/cgltf-tangents/README.md new file mode 100644 index 0000000..2a68b27 --- /dev/null +++ b/contrib/cgltf-tangents/README.md @@ -0,0 +1,42 @@ +# cgltf-tangents + +A library to compute missing tangent vectors in glTF models using MikkTSpace. + +## Example + +``` +// Load the glTF scene and buffers as usual. +cgltf_result result = cgltf_parse_file(&options, filepath, &data); +cgltf_load_buffers(&options, data, filepath); + +// Compute missing tangents. +cgltfTangentBuffer* tangent_buffers = 0; +cgltf_size num_tangent_buffers = 0; +cgltf_compute_tangents(&options, data, &tangent_buffers, &num_tangent_buffers); +``` + +## About + +This is a single-header/source library that combines +[MikkTSpace](https://github.com/mmikk/MikkTSpace) and +[cgltf](https://github.com/jkuhlmann/cgltf) to compute missing tangent vectors +for models. + +Mesh primitives in glTF may have a normal map but not necessarily tangent +vectors. An example is the +[DamagedHelmet](https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/DamagedHelmet/glTF) +sample. From the +[spec](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes): + +*"When tangents are not specified, client implementations SHOULD calculate +tangents using default MikkTSpace algorithms with the specified vertex +positions, normals, and texture coordinates associated with the normal texture."* + +cgltf-tangents takes an input glTF scene and scans it for mesh primitives that +have a normal map but no tangents. cgltf-tangents then invokes MikkTSpace to +compute tangents for those mesh primitives and outputs an array of tangent +buffers. The client can then upload these buffers to GPU memory for rendering. + +See `test/` for a complete example. + +MikkTSpace is packaged here for convenience. cgltf must be obtained separately. diff --git a/contrib/cgltf-tangents/cgltf_tangents.c b/contrib/cgltf-tangents/cgltf_tangents.c new file mode 100644 index 0000000..80b1e56 --- /dev/null +++ b/contrib/cgltf-tangents/cgltf_tangents.c @@ -0,0 +1,618 @@ +/* +Copyright 2022 Marc Sunet + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. 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. + +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 HOLDER 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 "cgltf_tangents.h" +#include "cgltf.h" + +#include "MikkTSpace/mikktspace.h" + +#include +#include +#include +#include +#include + +#ifdef CGLTF_TANGENTS_DEBUG +#include +#define DLOG printf +#else +#define DLOG(...) +#endif + +#include // TODO: Remove me. + +#define CGLTF_OPTIONS_MALLOC(size) \ + options->memory.alloc(options->memory.user_data, size) + +#define CGLTF_OPTIONS_FREE(ptr) \ + options->memory.free(options->memory.user_data, ptr) + +static void* cgltf_default_alloc(void* user, cgltf_size size) { + (void)user; + return malloc(size); +} + +static void cgltf_default_free(void* user, void* ptr) { + (void)user; + free(ptr); +} + +static const cgltf_size NUM_TANGENT_COMPONENTS = 4; // X,Y,Z,fSign + +static float normalize_i8(int8_t x) { return (float)x / 128.0; } +static float normalize_u8(uint8_t x) { return (float)x / 255.0; } +static float normalize_i16(int16_t x) { return (float)x / 32768.0; } +static float normalize_u16(uint16_t x) { return (float)x / 65535.0; } +static float normalize_u32(uint32_t x) { return (float)x / 4294967295.0; } + +static cgltf_size num_vertex_attrib_components(cgltf_type type) { + switch (type) { + case cgltf_type_scalar: + return 1; + case cgltf_type_vec2: + return 2; + case cgltf_type_vec3: + return 3; + case cgltf_type_vec4: + return 4; + default: + assert(false); + return 0; + } +} + +static cgltf_size cgltf_component_type_size_bytes(cgltf_component_type type) { + switch (type) { + case cgltf_component_type_r_8: + return 1; + case cgltf_component_type_r_8u: + return 1; + case cgltf_component_type_r_16: + return 2; + case cgltf_component_type_r_16u: + return 2; + case cgltf_component_type_r_32u: + return 4; + case cgltf_component_type_r_32f: + return 4; + default: + assert(false); + return 0; + } +} + +static cgltf_size default_stride(cgltf_type type, + cgltf_component_type component_type) { + return num_vertex_attrib_components(type) * + cgltf_component_type_size_bytes(component_type); +} + +// ----------------------------------------------------------------------------- +// MikkTSpace interface + +// An array of values for a given vertex attribute or for vertex indices. +// For positions and normals, glTF mandates floats. +// Texcoords and indices can be different types and vary in size: 8-bit, 16-bit, +// or 32-bit. +// We store void* pointers so that we can do byte pointer arithmetic. +typedef struct Buffer { + const void* start; // X-coordinate of the first attribute. + const void* end; // One byte past the end of the buffer. + cgltf_size stride_bytes; // Stride in bytes between each value. + cgltf_component_type type; // Type of each value in the buffer. +} Buffer; + +// User data for mesh processing. +// See: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes +// Buffer pointers have the accessor + view offsets baked in so that we do the +// addition only once. +typedef struct SMikkUserData { + const cgltf_primitive* primitive; + // Index buffer may be empty (mesh primitive has no indices). + Buffer indices; + // Vertex attributes. + Buffer positions; + Buffer normals; + Buffer texcoords; + // Output tangents. + void* tangents; +} SMikkUserData; + +static cgltf_size get_vertex_index(const SMikkUserData* data, cgltf_size iFace, + cgltf_size iVert) { + const cgltf_primitive* primitive = data->primitive; + + // First compute a vertex index as if the mesh primitive had no indices. + cgltf_size vertex_idx = 0; + switch (primitive->type) { + case cgltf_primitive_type_triangles: + vertex_idx = iFace * 3 + iVert; + break; + case cgltf_primitive_type_triangle_strip: + // For triangle strips: + // face 0 -> verts 0, 1, 2 + // face 1 -> verts 1, 3, 2 (1, 2, 3 flipped) + // face 2 -> verts 2, 3, 4 + // face 3 -> verts 3, 5, 4 (3, 4, 5 flipped) + // ... + // face N=2k -> verts N, N+1, N+2 + // face N=2k+1 -> verts N, N+2, N+1 + if (iFace & 1) { + // Flip the winding of odd faces so that the is consistent with the even + // ones. + // iVert = 0 -> vert 0 + // iVert = 1 -> vert 2 + // iVert = 2 -> vert 1 + vertex_idx = iFace + (2 - iVert); + } else { + vertex_idx = iFace + iVert; + } + break; + case cgltf_primitive_type_triangle_fan: + // For triangle fans: + // face 0 -> verts 0, 1, 2 + // face 1 -> verts 0, 2, 3 + // face 2 -> verts 0, 3, 4 + // face 3 -> verts 0, 4, 5 + // ... + // face N -> verts 0, N=1, N=2 + if (iVert == 0) { + vertex_idx = 0; + } else { + vertex_idx = iFace + iVert; + } + break; + default: + assert(false); + break; + } + + // If the mesh primitive has vertex indices, then vertex_idx is actually the + // index of the index. Index the index buffer with vertex_idx to find the + // real vertex index. + if (primitive->indices != NULL) { + const void* p_idx = + data->indices.start + vertex_idx * data->indices.stride_bytes; + switch (data->indices.type) { + case cgltf_component_type_r_8: + vertex_idx = *((int8_t*)p_idx); + break; + case cgltf_component_type_r_8u: + vertex_idx = *((uint8_t*)p_idx); + break; + case cgltf_component_type_r_16: + vertex_idx = *((int16_t*)p_idx); + break; + case cgltf_component_type_r_16u: + vertex_idx = *((uint16_t*)p_idx); + break; + case cgltf_component_type_r_32u: + vertex_idx = *((uint32_t*)p_idx); + break; + default: + assert(false); + break; + } + } + + return vertex_idx; +} + +static const void* get_vertex(const Buffer* buffer, cgltf_size index) { + // Stride is the offset in bytes between vertex attributes. + const void* vertex = buffer->start + buffer->stride_bytes * index; + assert(vertex < buffer->end); + return vertex; +} + +static const void* get_position(const SMikkUserData* data, cgltf_size index) { + return get_vertex(&data->positions, index); +} + +static const void* get_normal(const SMikkUserData* data, cgltf_size index) { + return get_vertex(&data->normals, index); +} + +static const void* get_texcoord(const SMikkUserData* data, cgltf_size index) { + return get_vertex(&data->texcoords, index); +} + +static float* get_tangent(void* buffer, cgltf_size index) { + // Tangents are tightly packed. + return (float*)(buffer) + NUM_TANGENT_COMPONENTS * index; +} + +static int SMikk_get_num_faces(const SMikkTSpaceContext* pContext) { + SMikkUserData* data = (SMikkUserData*)pContext->m_pUserData; + const cgltf_primitive* primitive = data->primitive; + + // Find the number of effective vertices (vertices or indices) in the mesh + // primitive. + // + // https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes + // + // "All attribute accessors for a given primitive MUST have the same count. + // When indices property is not defined, attribute accessors' count indicates + // the number of vertices to render; when indices property is defined, it + // indicates the upper (exclusive) bound on the index values in the indices + // accessor, i.e., all index values MUST be less than attribute accessors' + // count." + const cgltf_size num_verts = (primitive->indices != NULL) + ? primitive->indices->count + : primitive->attributes_count; + + // Determine the number of faces given the number of vertices. + // + // We use the fact that glTF only supports triangles for faces. + // https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes + switch (primitive->type) { + case cgltf_primitive_type_triangles: + return (int)num_verts / 3; + case cgltf_primitive_type_triangle_strip: + case cgltf_primitive_type_triangle_fan: + return (int)num_verts - 2; + default: + return 0; + } +} + +int SMikk_get_num_vertices_of_face(const SMikkTSpaceContext* pContext, + const int iFace) { + // Triangles are the only faces supported by glTF. + // https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes + return 3; +} + +void SMikk_get_position(const SMikkTSpaceContext* pContext, float fvPosOut[], + const int iFace, const int iVert) { + const SMikkUserData* data = (SMikkUserData*)pContext->m_pUserData; + const cgltf_primitive* primitive = data->primitive; + + const cgltf_size idx = get_vertex_index(data, iFace, iVert); + const float* coord = get_position(data, idx); + fvPosOut[0] = *coord++; + fvPosOut[1] = *coord++; + fvPosOut[2] = *coord; + DLOG("Position (face: %d, vert: %d): %f, %f, %f; idx: %lu\n", iFace, iVert, + fvPosOut[0], fvPosOut[1], fvPosOut[2], idx); +} + +void SMikk_get_normal(const SMikkTSpaceContext* pContext, float fvNormOut[], + const int iFace, const int iVert) { + const SMikkUserData* data = (SMikkUserData*)pContext->m_pUserData; + const cgltf_primitive* primitive = data->primitive; + + const cgltf_size idx = get_vertex_index(data, iFace, iVert); + const float* coord = get_normal(data, idx); + fvNormOut[0] = *coord++; + fvNormOut[1] = *coord++; + fvNormOut[2] = *coord; + DLOG("Normal (face: %d, vert: %d): %f, %f, %f\n", iFace, iVert, fvNormOut[0], + fvNormOut[1], fvNormOut[2]); +} + +void SMikk_get_texcoord(const SMikkTSpaceContext* pContext, float fvTexcOut[], + const int iFace, const int iVert) { + const SMikkUserData* data = (SMikkUserData*)pContext->m_pUserData; + const cgltf_primitive* primitive = data->primitive; + + const cgltf_size idx = get_vertex_index(data, iFace, iVert); + const void* coord = get_texcoord(data, idx); + switch (data->texcoords.type) { + case cgltf_component_type_r_8: { + const int8_t* c = coord; + fvTexcOut[0] = normalize_i8(*c++); + fvTexcOut[1] = normalize_i8(*c); + break; + } + case cgltf_component_type_r_8u: { + const uint8_t* c = coord; + fvTexcOut[0] = normalize_u8(*c++); + fvTexcOut[1] = normalize_u8(*c); + break; + } + case cgltf_component_type_r_16: { + const int16_t* c = coord; + fvTexcOut[0] = normalize_i16(*c++); + fvTexcOut[1] = normalize_i16(*c); + break; + } + case cgltf_component_type_r_16u: { + const uint16_t* c = coord; + fvTexcOut[0] = normalize_u16(*c++); + fvTexcOut[1] = normalize_u16(*c); + break; + } + case cgltf_component_type_r_32u: { + const uint32_t* c = coord; + fvTexcOut[0] = normalize_u32(*c++); + fvTexcOut[1] = normalize_u32(*c); + break; + } + case cgltf_component_type_r_32f: { + const float* c = coord; + fvTexcOut[0] = *c++; + fvTexcOut[1] = *c; + break; + } + default: + assert(false); + break; + } + DLOG("Texcoord (face: %d, vert: %d): %f, %f\n", iFace, iVert, fvTexcOut[0], + fvTexcOut[1]); +} + +void SMikk_set_TSpace_basic(const SMikkTSpaceContext* pContext, + const float fvTangent[], const float fSign, + const int iFace, const int iVert) { + SMikkUserData* data = (SMikkUserData*)pContext->m_pUserData; + const cgltf_primitive* primitive = data->primitive; + + const cgltf_size idx = get_vertex_index(data, iFace, iVert); + float* coord = get_tangent(data->tangents, idx); + *coord++ = fvTangent[0]; + *coord++ = fvTangent[1]; + *coord++ = fvTangent[2]; + *coord = fSign; + DLOG("Tangent (face: %d, vert: %d): %f, %f, %f; sign: %f\n", iFace, iVert, + fvTangent[0], fvTangent[1], fvTangent[2], fSign); +} + +// ----------------------------------------------------------------------------- + +static bool has_normal_map(const cgltf_primitive* primitive) { + return (primitive->material != NULL) && + (primitive->material->normal_texture.texture != NULL); +} + +static const cgltf_attribute* find_attribute(const cgltf_primitive* primitive, + cgltf_attribute_type type) { + for (cgltf_size i = 0; i < primitive->attributes_count; ++i) { + const cgltf_attribute* attrib = &primitive->attributes[i]; + if (attrib->type == type) { + return attrib; + } + } + return NULL; +} + +static bool has_attribute(const cgltf_primitive* primitive, + cgltf_attribute_type type) { + return find_attribute(primitive, type) != NULL; +} + +static bool has_positions3d(const cgltf_primitive* primitive) { + const cgltf_attribute* attrib = + find_attribute(primitive, cgltf_attribute_type_position); + if (attrib) { + return attrib->data->type == cgltf_type_vec3; + } + return false; +} + +static bool has_normals(const cgltf_primitive* primitive) { + return has_attribute(primitive, cgltf_attribute_type_normal); +} + +static bool has_texcoords(const cgltf_primitive* primitive) { + return has_attribute(primitive, cgltf_attribute_type_texcoord); +} + +static bool has_tangents(const cgltf_primitive* primitive) { + return has_attribute(primitive, cgltf_attribute_type_tangent); +} + +static bool has_indices(const cgltf_primitive* primitive) { + return primitive->indices != 0; +} + +static cgltfTangentBuffer compute_tangents(const cgltf_options* options, + const cgltf_data* data, + cgltf_primitive* primitive) { + cgltfTangentBuffer buffer = {0}; + SMikkUserData user = {0}; + cgltf_size num_verts = 0; + + user.primitive = primitive; + + if (primitive->indices != NULL) { + const cgltf_accessor* accessor = primitive->indices; + const cgltf_buffer_view* view = accessor->buffer_view; + const cgltf_size offset_bytes = accessor->offset + view->offset; + const void* buffer_data = view->buffer->data + offset_bytes; + const void* buffer_end = view->buffer->data + view->offset + view->size; + + user.indices.start = buffer_data; + user.indices.end = buffer_end; + // Indices are tightly packed, stride 0. + user.indices.stride_bytes = + default_stride(accessor->type, accessor->component_type); + user.indices.type = accessor->component_type; + } + + for (cgltf_size i = 0; i < primitive->attributes_count; ++i) { + const cgltf_attribute* attrib = &primitive->attributes[i]; + + if ((attrib->type == cgltf_attribute_type_position) || + (attrib->type == cgltf_attribute_type_normal) || + (attrib->type == cgltf_attribute_type_texcoord)) { + const cgltf_accessor* accessor = attrib->data; + const cgltf_buffer_view* view = accessor->buffer_view; + const cgltf_buffer* buffer = view->buffer; + const cgltf_size offset_bytes = accessor->offset + view->offset; + const cgltf_size stride_bytes = + view->stride > 0 + ? view->stride + : default_stride(accessor->type, accessor->component_type); + // const cgltf_size size_bytes = view->size; + const void* buffer_data = view->buffer->data + offset_bytes; + const void* buffer_end = view->buffer->data + view->offset + view->size; + + Buffer* attrib_buffer = 0; + + if (attrib->type == cgltf_attribute_type_position) { + // glTF currently mandates vec3 for positions. Caller should ensure + // this. + assert(accessor->type == cgltf_type_vec3); + num_verts = attrib->data->count; + attrib_buffer = &user.positions; + } else if (attrib->type == cgltf_attribute_type_normal) { + attrib_buffer = &user.normals; + } else if (attrib->type == cgltf_attribute_type_texcoord) { + attrib_buffer = &user.texcoords; + } + + attrib_buffer->start = buffer_data; + attrib_buffer->end = buffer_end; + attrib_buffer->stride_bytes = stride_bytes; + attrib_buffer->type = accessor->component_type; + } + } + + assert(user.positions.start); + assert(user.positions.end); + assert(user.normals.start); + assert(user.normals.end); + assert(user.texcoords.start); + assert(user.texcoords.end); + assert(num_verts > 0); + + const cgltf_size tangents_size_bytes = + num_verts * NUM_TANGENT_COMPONENTS * sizeof(float); + + user.tangents = CGLTF_OPTIONS_MALLOC(tangents_size_bytes); + if (!user.tangents) { + return buffer; + } + + SMikkTSpaceInterface interface = (SMikkTSpaceInterface){ + .m_getNumFaces = SMikk_get_num_faces, + .m_getNumVerticesOfFace = SMikk_get_num_vertices_of_face, + .m_getPosition = SMikk_get_position, + .m_getNormal = SMikk_get_normal, + .m_getTexCoord = SMikk_get_texcoord, + .m_setTSpaceBasic = SMikk_set_TSpace_basic, + }; + const SMikkTSpaceContext context = (SMikkTSpaceContext){ + .m_pInterface = &interface, + .m_pUserData = &user, + }; + if (!genTangSpaceDefault(&context)) { + return buffer; + } + + buffer.data = user.tangents; + buffer.size_bytes = tangents_size_bytes; + buffer.primitive = primitive; + + return buffer; +} + +static void process_primitive(const cgltf_options* options, + const cgltf_data* data, + cgltf_primitive* primitive, + cgltfTangentBuffer* tangent_buffers, + cgltf_size* num_tangent_buffers) { + DLOG("Processing primitive\n"); + cgltf_size cur_buffer = 0; + // TODO: MikkTSpace should not be used with models with vertex indices. One + // workaround is to unindex the mesh, compute tangents, and then re-index it. + if (((primitive->type == cgltf_primitive_type_triangle_fan) || + (primitive->type == cgltf_primitive_type_triangle_strip) || + (primitive->type == cgltf_primitive_type_triangles)) && + has_normal_map(primitive) && !has_tangents(primitive) && + has_positions3d(primitive) && has_normals(primitive) && + has_texcoords(primitive) && !has_indices(primitive)) { + *num_tangent_buffers += 1; + if (tangent_buffers) { + DLOG("Model with normal map missing tangents detected\n"); + tangent_buffers[cur_buffer] = compute_tangents(options, data, primitive); + if (tangent_buffers[cur_buffer].data) { + DLOG("Tangents computed\n"); + } + cur_buffer++; + } + } +} + +cgltf_result cgltf_compute_tangents(const cgltf_options* input_options, + const cgltf_data* data, + cgltfTangentBuffer** tangent_buffers, + cgltf_size* num_tangent_buffers) { + if ((input_options == NULL) || (data == NULL)) { + return cgltf_result_invalid_options; + } + + DLOG("cgltf_compute_tangents\n"); + + cgltf_options options = *input_options; + if (options.memory.alloc == NULL) { + options.memory.alloc = &cgltf_default_alloc; + } + if (options.memory.free == NULL) { + options.memory.free = &cgltf_default_free; + } + + // First pass: compute the number of tangent buffers to be created. + *num_tangent_buffers = 0; + for (cgltf_size mesh_idx = 0; mesh_idx < data->meshes_count; ++mesh_idx) { + const cgltf_mesh* mesh = &data->meshes[mesh_idx]; + + for (cgltf_size prim_idx = 0; prim_idx < mesh->primitives_count; + ++prim_idx) { + // Pass in null for the tangent buffers to just compute the number of + // buffers. + process_primitive(&options, data, &mesh->primitives[prim_idx], 0, + num_tangent_buffers); + } + } + DLOG("Number of primitives to be patched: %lu\n", *num_tangent_buffers); + + // Second pass: compute the tangents. + if (*num_tangent_buffers > 0) { + *tangent_buffers = + options.memory.alloc(options.memory.user_data, + *num_tangent_buffers * sizeof(cgltfTangentBuffer)); + if (!*tangent_buffers) { + return cgltf_result_out_of_memory; + } + + cgltf_size tangent_buffers_computed = 0; + + for (cgltf_size mesh_idx = 0; mesh_idx < data->meshes_count; ++mesh_idx) { + const cgltf_mesh* mesh = &data->meshes[mesh_idx]; + + for (cgltf_size prim_idx = 0; prim_idx < mesh->primitives_count; + ++prim_idx) { + process_primitive(&options, data, &mesh->primitives[prim_idx], + *tangent_buffers, &tangent_buffers_computed); + } + } + + assert(tangent_buffers_computed == *num_tangent_buffers); + } + + return cgltf_result_success; +} diff --git a/contrib/cgltf-tangents/cgltf_tangents.h b/contrib/cgltf-tangents/cgltf_tangents.h new file mode 100644 index 0000000..79e3502 --- /dev/null +++ b/contrib/cgltf-tangents/cgltf_tangents.h @@ -0,0 +1,67 @@ +/* +Copyright 2022 Marc Sunet + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. 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. + +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 HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +#ifndef CGLTF_TANGENTS_H_INCLUDED__ +#define CGLTF_TANGENTS_H_INCLUDED__ + +#include + +/// A buffer that holds tangent vectors. +/// +/// Tangent vectors are tightly packed in the array. +/// +/// Tangent vectors have 4 coordinates: (X,Y,Z) for the vector, W for the sign. +/// The usual rules of MikkTSpace apply, namely that the bitangent should be +/// computed as: +/// +/// bitangent = tangent.w * cross(normal, tangent.xyz); +/// +/// Refer to the MikkTSpace documentation for more details. +/// +/// The primitive pointer points to the mesh primitive for which the tangents in +/// this buffer were computed. When your application loads mesh primitives, it +/// can scan the cgltfTangetBuffer array outputed by cgltf_compute_tangents() to +/// see whether tangents were computed for the mesh primitive. +typedef struct cgltfTangentBuffer { + void* data; // X-coordinate of the first tangent vector. + cgltf_size size_bytes; // Total Size of data in bytes. + cgltf_primitive* primitive; // The primitive these tangents belong to. +} cgltfTangentBuffer; + +/// Compute tangent vectors for normal-mapped mesh primitives missing them. +/// +/// cgltf_options can be zeroed out but must be non-null. +/// +/// cgltf_data is the scene previously loaded by cgltf. +/// +/// out_tangent_buffers is an output array of tangent buffers, one buffer per +/// mesh primitive for which tangents were computed. +/// +/// out_num_tangent_buffers is the number of tangent buffers in the output +/// array. +cgltf_result cgltf_compute_tangents(const cgltf_options*, const cgltf_data*, + cgltfTangentBuffer** out_tangent_buffers, + cgltf_size* out_num_tangent_buffers); + +#endif // CGLTF_TANGENTS_H_INCLUDED__ diff --git a/contrib/cgltf-tangents/test/CMakeLists.txt b/contrib/cgltf-tangents/test/CMakeLists.txt new file mode 100644 index 0000000..422c950 --- /dev/null +++ b/contrib/cgltf-tangents/test/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.0) + +project (cgltf-test) + +add_executable(cgltf-test + main.c) + +target_link_libraries(cgltf-test + cgltf + cgltf-tangents + -lm) diff --git a/contrib/cgltf-tangents/test/main.c b/contrib/cgltf-tangents/test/main.c new file mode 100644 index 0000000..0d70008 --- /dev/null +++ b/contrib/cgltf-tangents/test/main.c @@ -0,0 +1,86 @@ +/* +Copyright 2022 Marc Sunet + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. 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. + +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 HOLDER 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 +#define CGLTF_IMPLEMENTATION +#include + +#include + +void print_tangent_buffer(const cgltfTangentBuffer* buffer, int max_vectors) { + printf("Tangent buffer for primitive (%p) (%lu bytes):\n", buffer->primitive, + buffer->size_bytes); + + const float* xyzw = (const float*)buffer->data; + const float* end = (const float*)(buffer->data + buffer->size_bytes); + + for (int i = 0; i < max_vectors && xyzw < end; ++i, xyzw += 4) { + printf("(%3.2f, %3.2f, %3.2f, sign: %3.2f)\n", *xyzw, *(xyzw + 1), + *(xyzw + 2), *(xyzw + 3)); + } + printf("--------------------"); +} + +void usage(const char* argv0) { + fprintf(stderr, "Usage: %s \n", argv0); +} + +int main(int argc, const char** argv) { + cgltf_options options = {0}; + cgltf_data* data = NULL; + + if (argc != 2) { + usage(argv[0]); + return 0; + } + + const char* filepath = argv[1]; + + cgltf_result result = cgltf_parse_file(&options, filepath, &data); + if (result != cgltf_result_success) { + cgltf_free(data); + return 1; + } + + // Must call cgltf_load_buffers() to load buffer data. + result = cgltf_load_buffers(&options, data, filepath); + if (result != cgltf_result_success) { + cgltf_free(data); + return 2; + } + + cgltfTangentBuffer* tangent_buffers = 0; + cgltf_size num_tangent_buffers = 0; + cgltf_compute_tangents(&options, data, &tangent_buffers, + &num_tangent_buffers); + + // cgltf scene not needed beyond this point. + cgltf_free(data); + + for (cgltf_size i = 0; i < num_tangent_buffers; ++i) { + print_tangent_buffer(tangent_buffers, 10); + } + + return 0; +} -- cgit v1.2.3