From dc8fe97715d47f5de0239ea73a0f700195011dd2 Mon Sep 17 00:00:00 2001 From: alteous Date: Tue, 29 Aug 2017 19:09:12 +0100 Subject: [PATCH] Add closures interface Former-commit-id: 76f4a864fa3b1fbde9d5ac2eab7c5d0639d147ef --- Cargo.toml | 13 +- build.rs | 9 + examples/generate.rs | 45 + libmikktspace/CMakeLists.txt | 11 + libmikktspace/mikktspace.c | 1890 ++++++++++++++++ libmikktspace/mikktspace.h | 145 ++ mikktspace-sys/src/lib.rs | 47 +- src/ffi.rs | 129 ++ src/lib.rs | 170 +- test-data/Avocado.bin | Bin 0 -> 23580 bytes test-data/Avocado.gltf | 185 ++ test-data/Avocado.json.REMOVED.git-id | 1 + test-data/Avocado.obj | 1900 +++++++++++++++++ test-data/AvocadoOut.obj.REMOVED.git-id | 1 + .../Avocado_baseColor.png.REMOVED.git-id | 1 + test-data/Avocado_normal.png.REMOVED.git-id | 1 + ...ocado_roughnessMetallic.png.REMOVED.git-id | 1 + test-data/gen | Bin 0 -> 56872 bytes test-data/gen2 | Bin 0 -> 62688 bytes test-data/generate-obj.rs | 45 + test-data/generate-test-data | Bin 0 -> 57056 bytes test-data/generate-test-data.c | 245 +++ test-data/generate-test-obj.c | 250 +++ test-data/libmikktspace.a | Bin 0 -> 48966 bytes test-data/mikktspace.h | 145 ++ 25 files changed, 5192 insertions(+), 42 deletions(-) create mode 100644 build.rs create mode 100644 examples/generate.rs create mode 100644 libmikktspace/CMakeLists.txt create mode 100644 libmikktspace/mikktspace.c create mode 100644 libmikktspace/mikktspace.h create mode 100644 src/ffi.rs create mode 100644 test-data/Avocado.bin create mode 100644 test-data/Avocado.gltf create mode 100644 test-data/Avocado.json.REMOVED.git-id create mode 100644 test-data/Avocado.obj create mode 100644 test-data/AvocadoOut.obj.REMOVED.git-id create mode 100644 test-data/Avocado_baseColor.png.REMOVED.git-id create mode 100644 test-data/Avocado_normal.png.REMOVED.git-id create mode 100644 test-data/Avocado_roughnessMetallic.png.REMOVED.git-id create mode 100755 test-data/gen create mode 100755 test-data/gen2 create mode 100644 test-data/generate-obj.rs create mode 100755 test-data/generate-test-data create mode 100644 test-data/generate-test-data.c create mode 100644 test-data/generate-test-obj.c create mode 100644 test-data/libmikktspace.a create mode 100644 test-data/mikktspace.h diff --git a/Cargo.toml b/Cargo.toml index 8802dbc6e5..16924db390 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,14 @@ [package] -name = "mikktspace" +name = "mikktspace-sys" version = "0.1.0" -authors = ["Benjamin Wasty "] +authors = ["alteous "] +build = "build.rs" + +[build-dependencies] +cmake = "0.1" [dependencies] -mikktspace-sys = { path = "./mikktspace-sys" } +gltf = "0.7" + +[[example]] +name = "generate" \ No newline at end of file diff --git a/build.rs b/build.rs new file mode 100644 index 0000000000..155a9ed067 --- /dev/null +++ b/build.rs @@ -0,0 +1,9 @@ + +extern crate cmake; + +fn main() { + let dst = cmake::build("libmikktspace"); + println!("cargo:rustc-link-search=native={}", dst.display()); + println!("cargo:rustc-link-lib=static=mikktspace"); +} + diff --git a/examples/generate.rs b/examples/generate.rs new file mode 100644 index 0000000000..ed4444a406 --- /dev/null +++ b/examples/generate.rs @@ -0,0 +1,45 @@ + +extern crate gltf; + +use std::io::Write; + +fn main() { + let path = "test-data/Avocado.gltf"; + let gltf = gltf::Import::from_path(path).sync().unwrap(); + let mesh = gltf.meshes().nth(0).unwrap(); + let primitive = mesh.primitives().nth(0).unwrap(); + let positions: Vec<[f32; 3]> = primitive.positions().unwrap().collect(); + let normals: Vec<[f32; 3]> = primitive.normals().unwrap().collect(); + let mut tex_coords: Vec<[f32; 2]> = vec![]; + let mut indices: Vec = vec![]; + match primitive.tex_coords(0).unwrap() { + gltf::mesh::TexCoords::F32(iter) => tex_coords.extend(iter), + _ => unreachable!(), + } + match primitive.indices().unwrap() { + gltf::mesh::Indices::U16(iter) => indices.extend(iter), + _ => unreachable!(), + } + + let file = std::fs::File::create("Avocado.obj").unwrap(); + let mut writer = std::io::BufWriter::new(file); + for position in &positions { + writeln!(writer, "v {} {} {}", position[0], position[1], position[2]); + } + for normal in &normals { + writeln!(writer, "vn {} {} {}", normal[0], normal[1], normal[2]); + } + for tex_coord in &tex_coords { + writeln!(writer, "vt {} {}", tex_coord[0], tex_coord[1]); + } + let mut i = indices.iter(); + while let (Some(v0), Some(v1), Some(v2)) = (i.next(), i.next(), i.next()) { + writeln!( + writer, + "f {}/{}/{} {}/{}/{} {}/{}/{}", + 1 + v0, 1 + v0, 1 + v0, + 1 + v1, 1 + v1, 1 + v1, + 1 + v2, 1 + v2, 1 + v2, + ); + } +} diff --git a/libmikktspace/CMakeLists.txt b/libmikktspace/CMakeLists.txt new file mode 100644 index 0000000000..376409d8c8 --- /dev/null +++ b/libmikktspace/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 2.8) +project(mikktspace) +set(PROJECT_VERSION_MAJOR "1") +set(PROJECT_VERSION_MINOR "0") +set(PROJECT_VERSION_PATCH "0") +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -std=c11") +set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS} ${CMAKE_C_FLAGS_DEBUG} -ggdb -DDEBUG") +set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS} ${CMAKE_C_FLAGS_RELEASE} -O2") +set(SOURCES mikktspace.h mikktspace.c) +add_library(mikktspace STATIC ${SOURCES}) +install(TARGETS mikktspace ARCHIVE DESTINATION ".") diff --git a/libmikktspace/mikktspace.c b/libmikktspace/mikktspace.c new file mode 100644 index 0000000000..62aa2da251 --- /dev/null +++ b/libmikktspace/mikktspace.c @@ -0,0 +1,1890 @@ +/** \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]; + else if (fvMax[c]dx && dy>dz) channel=1; + else if (dz>dx) channel=2; + + fSep = 0.5f*(fvMax[channel]+fvMin[channel]); + + // 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/mikktspace-sys/src/lib.rs b/mikktspace-sys/src/lib.rs index 84f7a6876b..f0644a98e5 100644 --- a/mikktspace-sys/src/lib.rs +++ b/mikktspace-sys/src/lib.rs @@ -4,7 +4,6 @@ mod ffi; use std::os::raw::*; use std::mem; -/// Rust FFI for the MikkTSpace implementation. const INTERFACE: ffi::SMikkTSpaceInterface = ffi::SMikkTSpaceInterface { m_getNumFaces: faces, m_getNumVerticesOfFace: vertices, @@ -16,26 +15,7 @@ const INTERFACE: ffi::SMikkTSpaceInterface = ffi::SMikkTSpaceInterface { }; pub struct Context { - faces: Box>, - positions: Box>, - tex_coords: Box>, - normals: Box>, -} - -/// Specifies whether a face is a triangle or a quad. -pub enum Face { - Triangle, - Quad, -} - -impl Face { - /// Returns the number of vertices bound by this face. - pub fn vertices(&self) -> i32 { - match self { - &Face::Triangle { .. } => 3, - &Face::Quad { .. } => 4, - } - } + faces: i32, } /// Returns the number of faces (triangles/quads) on the mesh to be processed. @@ -43,7 +23,7 @@ impl Face { extern "C" fn faces(pContext: *const ffi::SMikkTSpaceContext) -> c_int { unsafe { let m: *const Context = mem::transmute(pContext); - (*m).faces.len() as c_int + (*m).faces as c_int } } @@ -56,7 +36,7 @@ extern "C" fn vertices( ) -> c_int { unsafe { let _: *const Context = mem::transmute(pContext); - 3 + unimplemented!() } } @@ -134,19 +114,16 @@ extern "C" fn set_tspace( } impl Context { - /// Constructor for a MikkTSpace `Context`. - pub fn new(faces: F, positions: P, tex_coords: T, normals: N) -> Self - where - F: 'static + ExactSizeIterator, - P: 'static + ExactSizeIterator, - T: 'static + ExactSizeIterator, - N: 'static + ExactSizeIterator, - { + pub fn new() -> Self { Context { - faces: Box::new(faces), - positions: Box::new(positions), - tex_coords: Box::new(tex_coords), - normals: Box::new(normals), + faces: 3, } } } + +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + } +} diff --git a/src/ffi.rs b/src/ffi.rs new file mode 100644 index 0000000000..05e700bf30 --- /dev/null +++ b/src/ffi.rs @@ -0,0 +1,129 @@ + +#![allow(bad_style)] +#![allow(dead_code)] + +use std::os::raw::*; + +pub type tbool = c_int; +pub const TFALSE: tbool = 0; +pub const TTRUE: tbool = 1; + +#[repr(C)] +pub struct SMikkTSpaceInterface { + /// Returns the number of faces (triangles/quads) on the mesh to be processed. + pub m_getNumFaces: extern "C" fn(pContext: *const SMikkTSpaceContext) -> c_int, + + /// Returns the number of vertices on face number iFace + /// iFace is a number in the range {0, 1, ..., getNumFaces()-1} + pub m_getNumVerticesOfFace: extern "C" fn( + pContext: *const SMikkTSpaceContext, + iFace: c_int, + ) -> c_int, + + /// Returns the position of the referenced face of vertex number + /// iVert, in the range {0,1,2} for triangles, and {0,1,2,3} for quads. + pub m_getPosition: extern "C" fn( + pContext: *const SMikkTSpaceContext, + fvPosOut: *mut c_float, + iFace: c_int, + iVert: c_int, + ), + + /// Returns the normal of the referenced face of vertex number + /// iVert, in the range {0,1,2} for triangles, and {0,1,2,3} for quads. + pub m_getNormal: extern "C" fn( + pContext: *const SMikkTSpaceContext, + fvNormOut: *mut c_float, + iFace: c_int, + iVert: c_int, + ), + + /// Returns the texcoord of the referenced face of vertex number + /// iVert, in the range {0,1,2} for triangles, and {0,1,2,3} for quads. + pub m_getTexCoord: extern "C" fn( + pContext: *const SMikkTSpaceContext, + fvTexcOut: *mut c_float, + iFace: c_int, + iVert: c_int, + ), + + /// 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. + pub m_setTSpaceBasic: extern "C" fn( + pContext: *mut SMikkTSpaceContext, + fvTangent: *const c_float, + fSign: *const c_float, + iFace: c_int, + iVert: c_int, + ), + + /// 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. + pub m_setTSpace: extern "C" fn( + pContext: *mut SMikkTSpaceContext, + fvTangent: *const c_float, + fvBiTangent: *const c_float, + fMagS: *const c_float, + fMagT: *const c_float, + bIsOrientationPreserving: tbool, + iFace: c_int, + iVert: c_int, + ), +} + +/// these are both thread safe! +/// Default (recommended) fAngularThreshold is 180 degrees (which means threshold disabled) +extern "system" { + pub fn genTangSpaceDefault(pContext: *const SMikkTSpaceContext) -> tbool; + #[allow(dead_code)] + pub fn genTangSpace(pContext: *const SMikkTSpaceContext, fAngularThreshold: c_float) -> tbool; +} + +/// 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. + +#[repr(C)] +pub struct SMikkTSpaceContext { + /// initialized with callback functions + pub m_pInterface: *const SMikkTSpaceInterface, + /// pointer to client side mesh data etc. (passed as the first parameter with every interface call) + pub m_pUserData: *mut c_void, +} diff --git a/src/lib.rs b/src/lib.rs index cdfbe1aa56..5f91648b90 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,168 @@ -#[cfg(test)] -mod tests { - #[test] - fn it_works() { +#![allow(bad_style)] + +mod ffi; + +use std::os::raw::*; +use std::mem; +use std::ptr; + +/// Rust FFI for the MikkTSpace implementation. +const INTERFACE: ffi::SMikkTSpaceInterface = ffi::SMikkTSpaceInterface { + m_getNumFaces: faces, + m_getNumVerticesOfFace: vertices, + m_getPosition: position, + m_getNormal: normal, + m_getTexCoord: tex_coord, + m_setTSpaceBasic: set_tspace_basic, + m_setTSpace: set_tspace, +}; + +struct Closures<'a> { + pub vertices_per_face: &'a Fn() -> usize, + + /// Returns the number of faces. + pub face_count: &'a Fn() -> usize, + + /// Returns the positions of the indexed face. + pub position: &'a Fn(usize, usize) -> &'a [f32; 3], + + /// Returns the normals of the indexed face. + pub normal: &'a Fn(usize, usize) -> &'a [f32; 3], + + /// Returns the texture co-ordinates of the indexed face. + pub tex_coord: &'a Fn(usize, usize) -> &'a [f32; 2], + + /// Sets the generated tangent for the indexed face. + pub set_tangent: &'a mut FnMut(usize, usize, [f32; 4]), +} + +/// Returns the number of faces (triangles/quads) on the mesh to be processed. +extern "C" fn faces(pContext: *const ffi::SMikkTSpaceContext) -> c_int { + unsafe { + let x = (*pContext).m_pUserData as *const Closures; + ((*x).face_count)() as c_int } } + +/// Returns the number of vertices on face number iFace +/// iFace is a number in the range {0, 1, ..., getNumFaces()-1} +extern "C" fn vertices( + pContext: *const ffi::SMikkTSpaceContext, + _iFace: c_int, +) -> c_int { + unsafe { + let x = (*pContext).m_pUserData as *const Closures; + ((*x).vertices_per_face)() as c_int + } +} + +/// Returns the position of the referenced face of vertex number +/// iVert, in the range {0,1,2} for triangles, and {0,1,2,3} for quads. +extern "C" fn position( + pContext: *const ffi::SMikkTSpaceContext, + fvPosOut: *mut c_float, + iFace: c_int, + iVert: c_int, +) { + unsafe { + let x = (*pContext).m_pUserData as *const Closures; + let slice = ((*x).position)(iFace as usize, iVert as usize); + let src = slice.as_ptr() as *const c_float; + ptr::copy_nonoverlapping::(src, fvPosOut, 3); + } +} + +/// Returns the normal of the referenced face of vertex number +/// iVert, in the range {0,1,2} for triangles, and {0,1,2,3} for quads. +extern "C" fn normal( + pContext: *const ffi::SMikkTSpaceContext, + fvNormOut: *mut c_float, + iFace: c_int, + iVert: c_int, +) { + unsafe { + let x = (*pContext).m_pUserData as *const Closures; + let slice = ((*x).normal)(iFace as usize, iVert as usize); + let src = slice.as_ptr() as *const c_float; + ptr::copy_nonoverlapping::(src, fvNormOut, 3); + } +} + +/// Returns the texcoord of the referenced face of vertex number +/// iVert, in the range {0,1,2} for triangles, and {0,1,2,3} for quads. +extern "C" fn tex_coord( + pContext: *const ffi::SMikkTSpaceContext, + fvTexcOut: *mut c_float, + iFace: c_int, + iVert: c_int, +) { + unsafe { + let x = (*pContext).m_pUserData as *const Closures; + let slice = ((*x).tex_coord)(iFace as usize, iVert as usize); + let src = slice.as_ptr() as *const c_float; + ptr::copy_nonoverlapping::(src, fvTexcOut, 2); + } +} + +/// Returns the tangent and its sign to the application. +extern "C" fn set_tspace_basic( + pContext: *mut ffi::SMikkTSpaceContext, + fvTangent: *const c_float, + fSign: *const c_float, + iFace: c_int, + iVert: c_int, +) { + unsafe { + let x = (*pContext).m_pUserData as *mut Closures; + let mut tangent: [f32; 4] = mem::uninitialized(); + let dst: *mut c_float = tangent.as_mut_ptr(); + ptr::copy_nonoverlapping::(fvTangent, dst, 3); + tangent[3] = *fSign; + ((*x).set_tangent)(iFace as usize, iVert as usize, tangent); + } +} + +/// Returns tangent space results to the application. +extern "C" fn set_tspace( + _pContext: *mut ffi::SMikkTSpaceContext, + _fvTangent: *const c_float, + _fvBiTangent: *const c_float, + _fMagS: *const c_float, + _fMagT: *const c_float, + _bIsOrientationPreserving: ffi::tbool, + _iFace: c_int, + _iVert: c_int, +) { + unimplemented!() +} + +impl<'a> Closures<'a> { + pub fn generate(mut self) -> bool { + let ctx = ffi::SMikkTSpaceContext { + m_pInterface: &INTERFACE, + m_pUserData: &mut self as *mut Closures as *mut c_void, + }; + unsafe { + ffi::genTangSpaceDefault(&ctx) == ffi::TTRUE + } + } +} + +pub fn generate<'a>( + vertices_per_face: &'a Fn() -> usize, + face_count: &'a Fn() -> usize, + position: &'a Fn(usize, usize) -> &'a [f32; 3], + normal: &'a Fn(usize, usize) -> &'a [f32; 3], + tex_coord: &'a Fn(usize, usize) -> &'a [f32; 2], + set_tangent: &'a mut FnMut(usize, usize, [f32; 4]), +) -> bool { + let closures = Closures { + vertices_per_face, + face_count, + position, + normal, + tex_coord, + set_tangent, + }; + closures.generate() +} diff --git a/test-data/Avocado.bin b/test-data/Avocado.bin new file mode 100644 index 0000000000000000000000000000000000000000..79c28dec5fca03ae13abeda5b06f4c253ac96678 GIT binary patch literal 23580 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⪻TYc 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 HsrmnZ(R`E+ literal 0 HcmV?d00001 diff --git a/test-data/generate-obj.rs b/test-data/generate-obj.rs new file mode 100644 index 0000000000..f226a2b9ef --- /dev/null +++ b/test-data/generate-obj.rs @@ -0,0 +1,45 @@ + +extern crate gltf; + +use std::io::Write; + +fn main() { + let path = "test-data/Avocado.gltf"; + let gltf = gltf::Import::from_path(path).sync().unwrap(); + let mesh = gltf.meshes().nth(0).unwrap(); + let primitive = mesh.primitives().nth(0).unwrap(); + let positions: Vec<[f32; 3]> = primitive.positions().unwrap().collect(); + let normals: Vec<[f32; 3]> = primitive.normals().unwrap().collect(); + let mut tex_coords: Vec<[f32; 2]> = vec![]; + let mut indices: Vec = vec![]; + match primitive.tex_coords(0).unwrap() { + gltf::mesh::TexCoords::F32(iter) => tex_coords.extend(iter), + _ => unreachable!(), + } + match primitive.indices().unwrap() { + gltf::mesh::Indices::U16(iter) => indices.extend(iter), + _ => unreachable!(), + } + + let file = std::fs::File::create("Avocado.obj").unwrap(); + let mut writer = std::io::BufWriter::new(file); + for position in &positions { + writeln!(writer, "v {} {} {}", position[0], position[1], position[2]); + } + for normal in &normals { + writeln!(writer, "vn {} {} {}", normal[0], normal[1], normal[2]); + } + for tex_coord in &tex_coords { + writeln!(writer, "vt {} {}", tex_coord[0], tex_coord[1]); + } + let mut i = indices.iter(); + while let (Some(v0), Some(v1), Some(v2)) = (i.next(), i.next(), i.next()) { + writeln!( + writer, + "f {}/{}/{} {}/{}/{} {}/{}/{}", + v0, v0, v0, + v1, v1, v1, + v2, v2, v2, + ); + } +} diff --git a/test-data/generate-test-data b/test-data/generate-test-data new file mode 100755 index 0000000000000000000000000000000000000000..b383add654c23d9bd041048b6afa9a78aaf2c91a GIT binary patch literal 57056 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 +#include +#include +#include +#include +#include + +#include "mikktspace.h" + +struct tangent { + // Vector. + float v[3]; + + // Sign. + float s; +}; + +struct vertex { + // Borrows from `input.positions`. + float (*position)[3]; + + // Borrows from `input.positions`. + float (*normal)[3]; + + // Borrows from `input.positions`. + float (*tex_coord)[2]; +}; + +struct face { + // Borrows from `positions`, `normals`, and `tex_coords`. + struct vertex vertices[3]; +}; + +struct input { + // Owned. + float (*positions)[3]; + + // Owned. + float (*normals)[3]; + + // Owned. + float (*tex_coords)[2]; + + // Borrows from `positions`, `normals`, and `tex_coords`. + struct face *faces; + + // Number of entries in `positions`, `normals`, and `tex_coords`. + size_t nr_vertices; + + // Number of entries in `faces`. + size_t nr_faces; +} input; + +struct output { + // Borrows from `positions`, `normals`, and `tex_coords`. + struct vertex *vertices; + + // Owned. + struct tangent *tangents; + + // Number of entries in `vertices` and `tangents`. + // Equal to `3 * input.nr_faces`. + size_t nr_vertices; +} output; + +void print_vec2(float (*t)[2]) { + printf("[%f, %f]", (*t)[0], (*t)[1]); +} + +void print_vec3(float (*t)[3]) { + printf("[%f, %f, %f]", (*t)[0], (*t)[1], (*t)[2]); +} + +void print_tangent(const struct tangent *t) { + printf("[%f, %f, %f, %f]", t->v[0], t->v[1], t->v[2], t->s); +} + +int get_num_faces(const SMikkTSpaceContext *x) { + return input.nr_faces; +} + +int get_num_vertices_of_face(const SMikkTSpaceContext *x, int f) { + return 3; +} + +void get_position(const SMikkTSpaceContext *x, float *dst, int f, int v) { + float (*src)[3] = input.faces[f].vertices[v].position; + memcpy(dst, src, sizeof(*src)); +} + +void get_normal(const SMikkTSpaceContext *x, float *dst, int f, int v) { + float (*src)[3] = input.faces[f].vertices[v].normal; + memcpy(dst, src, sizeof(*src)); +} + +void get_tex_coord(const SMikkTSpaceContext *x, float *dst, int f, int v) { + float (*src)[2] = input.faces[f].vertices[v].tex_coord; + memcpy(dst, src, sizeof(*src)); +} + +void set_tspace_basic( + const SMikkTSpaceContext *x, + const float *t, + float s, + int f, + int v +) { + // The index of the last output (vertex, tangent) pair. + static int i = 0; + + struct vertex *in = &input.faces[f].vertices[v]; + + output.vertices[i].position = in->position; + output.vertices[i].normal = in->normal; + output.vertices[i].tex_coord = in->tex_coord; + memcpy(output.tangents[i].v, t, 3 * sizeof(float)); + output.tangents[i].s = s; + + ++i; +} + +void set_tspace( + const SMikkTSpaceContext *x, + const float *t, + const float *b, + float mag_s, + float mag_t, + tbool op, + int f, + int v +) { + assert(!"unreachable"); +} + +int main() { + input.nr_vertices = 406; + input.nr_faces = 682; + output.nr_vertices = 3 * input.nr_faces; + + input.positions = calloc(input.nr_vertices, sizeof(*input.positions)); + input.normals = calloc(input.nr_vertices, sizeof(*input.normals)); + input.tex_coords = calloc(input.nr_vertices, sizeof(*input.tex_coords)); + input.faces = calloc(input.nr_faces, sizeof(*input.faces)); + output.vertices = calloc(output.nr_vertices, sizeof(*output.vertices)); + output.tangents = calloc(output.nr_vertices, sizeof(*output.tangents)); + + FILE *fi = fopen("Avocado.obj", "rb"); + assert(fi); + char buffer[1024]; + + for (size_t i = 0; i < input.nr_vertices; ++i) { + fgets(buffer, sizeof(buffer), fi); + sscanf( + buffer, + "v %f %f %f", + &input.positions[i][0], + &input.positions[i][1], + &input.positions[i][2] + ); + } + + for (size_t i = 0; i < input.nr_vertices; ++i) { + fgets(buffer, sizeof(buffer), fi); + sscanf( + buffer, + "vn %f %f %f", + &input.normals[i][0], + &input.normals[i][1], + &input.normals[i][2] + ); + } + + for (size_t i = 0; i < input.nr_vertices; ++i) { + fgets(buffer, sizeof(buffer), fi); + sscanf( + buffer, + "vt %f %f", + &input.tex_coords[i][0], + &input.tex_coords[i][1] + ); + } + + for (size_t i = 0; i < input.nr_faces; ++i) { + fgets(buffer, sizeof(buffer), fi); + int v[3]; + sscanf( + buffer, + "f %d/%d/%d %d/%d/%d %d/%d/%d", + &v[0], &v[0], &v[0], + &v[1], &v[1], &v[1], + &v[2], &v[2], &v[2] + ); + for (size_t j = 0; j < 3; ++j) { + input.faces[i].vertices[j].position = &input.positions[v[j] - 1]; + input.faces[i].vertices[j].normal = &input.normals[v[j] - 1]; + input.faces[i].vertices[j].tex_coord = &input.tex_coords[v[j] - 1]; + } + } + + SMikkTSpaceInterface interface = { + .m_getNumFaces = get_num_faces, + .m_getNumVerticesOfFace = get_num_vertices_of_face, + .m_getPosition = get_position, + .m_getNormal = get_normal, + .m_getTexCoord = get_tex_coord, + .m_setTSpaceBasic = set_tspace_basic, + .m_setTSpace = NULL, + }; + SMikkTSpaceContext context = { + .m_pInterface = &interface, + .m_pUserData = NULL, + }; + + genTangSpaceDefault(&context); + + printf("{\n \"vlist\": [\n"); + for (size_t i = 0; i < output.nr_vertices; ++i) { + printf(" {\"v\": "); + print_vec3(output.vertices[i].position); + printf(", \"vn\": "); + print_vec3(output.vertices[i].normal); + printf(", \"vt\": "); + print_vec2(output.vertices[i].tex_coord); + printf(", \"vx\": "); + print_tangent(&output.tangents[i]); + if (i == output.nr_vertices - 1) { + printf("}\n"); + } else { + printf("},\n"); + } + } + printf(" ]\n}"); + + fclose(fi); + free(input.positions); + free(input.normals); + free(input.tex_coords); + free(input.faces); + free(output.vertices); + free(output.tangents); + + return 0; +} + diff --git a/test-data/generate-test-obj.c b/test-data/generate-test-obj.c new file mode 100644 index 0000000000..6d4a8ea921 --- /dev/null +++ b/test-data/generate-test-obj.c @@ -0,0 +1,250 @@ + +#include +#include +#include +#include +#include +#include + +#include "mikktspace.h" + +struct tangent { + // Vector. + float v[3]; + + // Sign. + float s; +}; + +struct vertex { + // Borrows from `input.positions`. + float (*position)[3]; + + // Borrows from `input.positions`. + float (*normal)[3]; + + // Borrows from `input.positions`. + float (*tex_coord)[2]; +}; + +struct face { + // Borrows from `positions`, `normals`, and `tex_coords`. + struct vertex vertices[3]; +}; + +struct input { + // Owned. + float (*positions)[3]; + + // Owned. + float (*normals)[3]; + + // Owned. + float (*tex_coords)[2]; + + // Borrows from `positions`, `normals`, and `tex_coords`. + struct face *faces; + + // Number of entries in `positions`, `normals`, and `tex_coords`. + size_t nr_vertices; + + // Number of entries in `faces`. + size_t nr_faces; +} input; + +struct output { + // Borrows from `positions`, `normals`, and `tex_coords`. + struct vertex *vertices; + + // Owned. + struct tangent *tangents; + + // Number of entries in `vertices` and `tangents`. + // Equal to `3 * input.nr_faces`. + size_t nr_vertices; +} output; + +void print_vec2(float (*t)[2]) { + printf("%f %f", (*t)[0], (*t)[1]); +} + +void print_vec3(float (*t)[3]) { + printf("%f %f %f", (*t)[0], (*t)[1], (*t)[2]); +} + +void print_tangent(const struct tangent *t) { + printf("%f %f %f %f", t->v[0], t->v[1], t->v[2], t->s); +} + +int get_num_faces(const SMikkTSpaceContext *x) { + return input.nr_faces; +} + +int get_num_vertices_of_face(const SMikkTSpaceContext *x, int f) { + return 3; +} + +void get_position(const SMikkTSpaceContext *x, float *dst, int f, int v) { + float (*src)[3] = input.faces[f].vertices[v].position; + memcpy(dst, src, sizeof(*src)); +} + +void get_normal(const SMikkTSpaceContext *x, float *dst, int f, int v) { + float (*src)[3] = input.faces[f].vertices[v].normal; + memcpy(dst, src, sizeof(*src)); +} + +void get_tex_coord(const SMikkTSpaceContext *x, float *dst, int f, int v) { + float (*src)[2] = input.faces[f].vertices[v].tex_coord; + memcpy(dst, src, sizeof(*src)); +} + +void set_tspace_basic( + const SMikkTSpaceContext *x, + const float *t, + float s, + int f, + int v +) { + // The index of the last output (vertex, tangent) pair. + static int i = 0; + + struct vertex *in = &input.faces[f].vertices[v]; + + output.vertices[i].position = in->position; + output.vertices[i].normal = in->normal; + output.vertices[i].tex_coord = in->tex_coord; + memcpy(output.tangents[i].v, t, 3 * sizeof(float)); + output.tangents[i].s = s; + + ++i; +} + +void set_tspace( + const SMikkTSpaceContext *x, + const float *t, + const float *b, + float mag_s, + float mag_t, + tbool op, + int f, + int v +) { + assert(!"unreachable"); +} + +int main() { + input.nr_vertices = 406; + input.nr_faces = 682; + output.nr_vertices = 3 * input.nr_faces; + + input.positions = calloc(input.nr_vertices, sizeof(*input.positions)); + input.normals = calloc(input.nr_vertices, sizeof(*input.normals)); + input.tex_coords = calloc(input.nr_vertices, sizeof(*input.tex_coords)); + input.faces = calloc(input.nr_faces, sizeof(*input.faces)); + output.vertices = calloc(output.nr_vertices, sizeof(*output.vertices)); + output.tangents = calloc(output.nr_vertices, sizeof(*output.tangents)); + + { + FILE *fi = fopen("Avocado.obj", "rb"); + assert(fi); + char buffer[1024]; + + for (size_t i = 0; i < input.nr_vertices; ++i) { + fgets(buffer, sizeof(buffer), fi); + sscanf( + buffer, + "v %f %f %f", + &input.positions[i][0], + &input.positions[i][1], + &input.positions[i][2] + ); + } + + for (size_t i = 0; i < input.nr_vertices; ++i) { + fgets(buffer, sizeof(buffer), fi); + sscanf( + buffer, + "vn %f %f %f", + &input.normals[i][0], + &input.normals[i][1], + &input.normals[i][2] + ); + } + + for (size_t i = 0; i < input.nr_vertices; ++i) { + fgets(buffer, sizeof(buffer), fi); + sscanf( + buffer, + "vt %f %f", + &input.tex_coords[i][0], + &input.tex_coords[i][1] + ); + } + + for (size_t i = 0; i < input.nr_faces; ++i) { + fgets(buffer, sizeof(buffer), fi); + int v[3]; + sscanf( + buffer, + "f %d/%d/%d %d/%d/%d %d/%d/%d", + &v[0], &v[0], &v[0], + &v[1], &v[1], &v[1], + &v[2], &v[2], &v[2] + ); + for (size_t j = 0; j < 3; ++j) { + input.faces[i].vertices[j].position = &input.positions[v[j] - 1]; + input.faces[i].vertices[j].normal = &input.normals[v[j] - 1]; + input.faces[i].vertices[j].tex_coord = &input.tex_coords[v[j] - 1]; + } + } + + fclose(fi); + } + + SMikkTSpaceInterface interface = { + .m_getNumFaces = get_num_faces, + .m_getNumVerticesOfFace = get_num_vertices_of_face, + .m_getPosition = get_position, + .m_getNormal = get_normal, + .m_getTexCoord = get_tex_coord, + .m_setTSpaceBasic = set_tspace_basic, + .m_setTSpace = NULL, + }; + SMikkTSpaceContext context = { + .m_pInterface = &interface, + .m_pUserData = NULL, + }; + + genTangSpaceDefault(&context); + + for (size_t i = 0; i < output.nr_vertices; ++i) { + printf("v "); + print_vec3(output.vertices[i].position); + printf("\n"); + } + + for (size_t i = 0; i < output.nr_vertices; ++i) { + printf("vn "); + print_vec3(output.vertices[i].normal); + printf("\n"); + } + + int k = 1; + for (size_t i = 0; i < output.nr_vertices / 3; ++i) { + int v0 = k++; + int v1 = k++; + int v2 = k++; + printf("f %d//%d %d//%d %d//%d\n", v0, v0, v1, v1, v2, v2); + } + + free(input.positions); + free(input.normals); + free(input.tex_coords); + free(input.faces); + free(output.vertices); + free(output.tangents); + + return 0; +} + diff --git a/test-data/libmikktspace.a b/test-data/libmikktspace.a new file mode 100644 index 0000000000000000000000000000000000000000..01df84eade6ff3622b01a389c92403b3e6a73055 GIT binary patch literal 48966 zcmchA4}4WemH*A3+7zkpQBmoZ{WVprscmU!X$uCr;m>U!T|y&}{*x901jsg!n7oHn z34}i0ZmyTms6?aI-MFHn^4n#1tFpDR0r~mTif&r5;%`|u^~bu&CcEO6U8Cai`+m>N z+;{KId&!IL?{`1RyK~Q+IdkUBnVBk&EoSH!d?4j2qQG%jXgORx>J~$sgiQ5RfNZ5J8N(sA;urb1h z2%ApWmB-1;$Q8$#<73~-me~L^JB!Tvm{~v50~EFK`0xpWz(lm_3_+(1=sZDZ2?D;t z8j890AbEG~A;%uW6Ir&$B4z!69(g^o$3BuT12!3`0t4$j25d4=1qKv|4A^9#3JfSv z{tU2C=P2&}NZCm+2NE+RONJh?&zVBor@cqX4_xWk{PU8kjg+0BWJp))Ml#DG(wNQy?dl)MCle(s9JJl&LFJlRfs((4Snse0hkq{i4)1l2!VWp)R3N ztSZWGNELpM-9Pk6BtumJdIb}|9(nwP#;P$`RsBQX{IAQGebiKmLRAeKAgFQ|T>j)# z>?)B3xbevX!Vbv-W%J8|g}B55@JINKtUVE#t3sdXi+rumli93s>_RaedlU)n(V@RY zsZ)K4GS|N7!9J~EGhKVkgHfodzIdQDdGd`qt(Tm{Sh97@+BF@&MSI_nY`tVXd3~(o zYRdAtzratd<4%TS9gBIB3UcjB9WzsWL{`bcqN@fpNv9R{nYiH8jJozHBAk;Hu@lA9 ziLpX?8grtZUKy)nPAeK&q7RMuI8gggxXS4W97iC=ejxJUe?&G6q+Uh9;LJ8SvjvAe zl;Hfe!U3%kk}4s7%#D&-NzAD_h8nIp7Aa$=!LWU7=&oOjfOeB$%#9JXa+XR~E$L=u zPxoKHl61?7$~4niwdc&!Y(9-DGErnU;8vD6_E|$0kU_U{7F2(D9lAN4DrQPfV(gou zQVY-^=}Zjqm??ZEav6`tT&Ga}&!HSXO&cKNPLUzM9YI zA*yHLegXGVkkqXoeGh64$_)KR{?a^SPTJo)?HlD_8FPAPp@pHmshPNMj=9C}ETax$ zAeq+}OQ)Iu#j}OrE-z@eDUjW+@14A-Q)L<>ow~&fm3yK2UZ~OwE%!nTyim*wt@J{R zbSUPg$ZyPr&f%|19-JRu9&l3CfTvSyH6lvg!4jF$D$t!T=~r(2W7mEP~z~0F?rim?oCM@R_S9DI=ee_XZG4+z!U*f;r!d zsiLg|EZNK7qQq565p%&vR6&XpS1L$hQm7WVa;p$|?EY-BS1M(%&iz5;?h1j}CaBav zte`JeFc3R2SZLHE=183fkQfatgIkorwcBFJUeS^Gm9b=No0YiB14K|XMtRtbkF$?x zGax0B6D44(gM6AAb-yYJHRir|SeBEb(zsm;t3xS)CJ!;ChYG!tIVoyYI<)}5N-xWG zX!tEKP2Ch9#}^-6y`onz;3B&XK+1vIQC%Fw-8>O4nya7gHsW@XI^ZmZ^D5c@$gP&; zYx_OyfsVLcq#VGj58|OynPDGyyGS*Fwo$!|y#{f(mVSXieX-OAOvzIiyp@R>_R=!_SM6 zoG~sca}4wi$T-={-=g@{Do`9r?L_KyYCa1`1zsZx%v^G*J_l>Z?uHz?Q7;zz{sH|^ zSFU zHQmeI)zg=|9V`GOeG3`yfCbx;08?UqV*0z>3`DBZFC)sAQyFur&VW|Bm1Shit(=n_ zD@c^M(pX2{pzbOww@c;Z<>KgpP35rU@<39m3PS)n!w|q*MT*jo9QZV}b)=v(i%k_d za696Vdq$B^A&^I6Y;)|6`C>lDuMpc(%_=E4%z9^3VJkn@*3|&@arh(DoN0^*__?Leu`fx=h4!V^h^Q{7x{($3rI8YeNDdX~dFj5< zK{Dj@E}*ELuX1Q}lr+*olRcKUyMYSp-tN)&BfO_&WKyA(NUcKEUTCcsVnb#u>W0~P zo$iS_0ego1qnHkTP|N|$Q!trSnNW7a|B4?y8g|F@)EpFre`St|iDk?oDUg^=j|Q{3 zz-&Zjv{kAuU{1Xgb5U{P&9M7Mt|gS6yDd@EyeNU4V(djP4ID!=>{BYu8DdJ5q>(6r zu?DNgyfnxg8igQ2Z&*ANT;eGsL{*1C&pBH+SCYJ<^G}^F~F1)x!wLCPM)w zR}@C>U>wv|)0m?Y(E-c|2q%*W;!ufn7m#!QVYiDu$GU^l61y`yxH~(zM_EB{5nChv zCfvF$nWOYri6#{nvXEet$n3;x*D4LMy3LpYP>;guF(BQ~xE<6=qqLm5rH8)!X6SbP z8!XGP=&5$dJQ*L~H3)J()kt+zM;!3ZGhYt3Rxa;W=RVq^UGWu^vbdfQllf`gln2BT)<>G7B=s3}) zqsfy6PIQ9i@HQrc=+hJcBv6fKYU9X*u7m?5yGgQ(D6vhi#7_LAFxGMZC5}C@f|uNB zp_pdIfQf&1uPJzmJP2v2gN16^{28CX43E_ddMi*H z?aBZ-mdQzZTalBn5X3$fD>$hH>|horMHtWYWZLp5I!~n|EYirjEIMLJFIs{e#+5$q zm3~(C(r3|>Pe_^cEQ6Ay?|_4%Phs!N!i=;5t3pusT%Qv`(aMtVQCE^mCs}N|B8;s^ zGC~;sqG+xuu2%)QE9`h~KI}GlZwev@{}43M2_CT``)@!n<8T}P5#;VF8{J=h$EkCc zWAAtE{eC6!2KQ*7(KxfSG#ZcWi9wW=bF?F%iq;`3JDFU^MRT0yw2bzO5tTnVyEfS3 z+2T)vUuWs`q{P8EB#8>UEplKM0+?WQ#L(Ki%1PKw9MS#$5~u%fsei1H7V2TCTh)fn z1`Zdo!ZaGAPhzE149sxn6FCs0AwU_+a0DNXa@?ZWnfbiW`~=(^pY%G$zR@V_Nlm0+ z0a|3!=^6uJr{f}impiEoxP|<^X>kIx0BJG4RcJO6Go&YjVl?&3ZXFEGK;<1w`z0(QGyql zD@1nwz7Ifs*f~9&A}*@K8sRdz@~~;_X)2QDqAO^k2O=&ek|$JsmQ^A#nS*IaoN5Ol zwp{0n0s(L*F;C4aVop>=ttVFrAiFBSS2$fQD~#_Jm1jatP1Yh)w~N1M-qOTmYVyL{ z{5_dAxM!r4bv6$B?pXosCQzfW36Yf==sPf2%&!FH8q`rdyqc`j5LlXT}6Ma-ivm%(fTcSU1qC0f7s)1-# z528Q#A^aqEl4x9~I!y3pg4YwwdLpqS1XmFp!y_vuT6K)Dg@nx~Y(HVo6828QN(pNx z>^NaF2t%VO1wn9PjbL?$HHLP~TZOddR``YlA(FZUx~v}(3v%iyy4fbFOyt0W_#JK+FaZ@(bh4|Sg6tm5#V(P@H$YTTPIiUY z1xS_@mkxRTpQHj_3AGZ}7>N-YIq(>M{gB=BbfMy9Iz`6R{b+VJjTH9Ntka1i`%il! z8D9W+?4sNk0LNen4AG$g170P5B71hZDiM=95f>(P&daeAuVLY>OIZ#t#oTEzrzGaC zMsC$5+Ce=|3&0f@%VA{7T6u}7WFVT}8kI-$rW|S!B2o*#4})X66?s=~MGcW* zcIY2o<-%`d|2~wTjfm;t|L0a1wSYTG`p-!ExQ(=61#Tpyg-~HiVkR<4+FF(NDu|zo zGg6$i+Y2CZOu$;FC0qAfk<@M|FGGpjv#Sf{ZwFWjW|ps3J?ZAacxShPT3dyf2$2I# zX!o7o%;^}*>-0)}I$zxY;W%RNWH!L0#YsWNjADC$;XdfL(euwy&%1V$0#|*(Q5%+i z;UWm>w@-DneoL-mMLvn~bo{;mQQZvEuYyRZAzDizqz}C*KnYc$^jnpKqZ%0R2D6uk z_d&m*>jVwIkGfvdZc1{_$iY7{(cWPCdX7=Z?yj&8(V*D{DL~VM-KnZFXk)i(32^Kn^EGG$ zNy!POCUv$#77QW>=4M*Q8g%s!gLgEKelUZszE5sl$8mEyp2crEHGp4gPC9kQ3!U{s z=e&@K9ge^gf-aFXWK$OxM;VFXOz4_jQ0D%Nhh&4Rp=P%PHXgv^ zlM;UEZXji6DIobLVTqij)jID;Y6hD`zUc>I$-cGNR8<0zB3ItNE#(ye42^GF1sV>_ z!f#uJ0n(-z*!00-h`c*y1Bd01H3E2t{uNs76y->#b}NMvQUQ_h*@+Xvx(eqAjbp+O zNy4NEpMd4ECR zV*Ds6!Uw=IeNo^SiQh$PD7@&O^uvpxW|@9i3_El};>2{jm`4CR62z`Vr4u`byW7Q_ z1K6)Yd_LN$8<`=au-ip`0@!~O#AYW++S_o~4oM8M%DbxOgLv%Vc#C282#B$F3+Wa1CkE*6+k~%wl5G{>eZ5LYTAGXYu9HwPNGMh-wot9$oyzJA|QLAgZVw+DWSv!-?By>OAO4QjHq|~jD&rutw3czv*b%Bv6NP%;EoxMbxUNiyBF zuiGUqyNC&Jy`Gl1>>%irAzci{KJpWFs%D^>r3%TLDx?6UkcDWK`U!>1HDEa2Kr_`s zD#imG`vnpxBz4{^q_omNx_nAns!Gb6Dyaax>eNsFPf9wK>I{^0%7rXbh2%|H3Igz| zQ$OWh$VnK7e?^V>89fYeoL_aiDH>-5VhXC{6{%+d*;8r`=G0=$&9c0V{Z?m~QV)1y z4#GARJ5ct*S4e#1>CGb*rA|(qBhnFy^)% zxy;^{T_83hF@&8r*j~gC_Royl9Ho0_`>Z;}2M6&U>4J0lV3PTBc};-JS^rg^-I{*L@&aUeT*>kREySXA^7A%HwngTB2{PNB$}| zY&hyG_9IRUB1+AZ5h9lO5nqD}`Y1}Vure*mBHPu}G0W$**2s}i1rkL9z7I<@3i9w_ z^)n$-Kf(=Ja=P;oZJFSYZ!SNKO)4)LnqYspVP`S!nxlrGz-lAIQnDm z*CV&CZrq%XL-;f1X?uI3vK^acq(Ur6lHekt?{nByJiFK!o zpMrp4^OI~I3E@hXgm5J%PZq|~#RKFe|KxgUyRvNRNp6{XlJco1!OC&IqA+Dazd6+; zi>98Wa_UL2PdGou6j)kT8B2Rs?lUTRz1xcr3o8EcyeppPSbgTj1Wi(_PkS6+GsvVl}|D=3Fpg@hxid#9N7%H&`HMBc@Bh^Wn}Q{*B@ z;361G$_@t$N9KF^h~pprck&>!6el7&8+9CIa)G1$8z#r5E-EnTBXCIwkW7X0U+=$C zW=4JFR#b0F^#J0<&^CC#NdR*-YnW@efdXbsFP|Mphab(=n6g)Yb%9N3hN!Yi40n@O zpnTFlC+VY~XH$sG9c;!nPs32MSIU-nqemBq_C(tp@%rlcc(!=DgI#e7Pp-@|U@^hu zsJbEjLI$P$l1z7fVT4G(}? z#WdT}JK-;*W5Dy5DIX79C&#cI3?oHV&vM7+<^Xlv&9TpCw7)L^0 z;!eP%eF|G1rpg9uTo!u2z#$=@Db-&Tw){lAlku4qB1+uLF`tz5$Iu&01h>=>xqMW;}~xGMvZSzlh})I863 zgt_+f6UwiTg9~1!2Rg^x%BbkDNnyGzwM_+1d_f&c+vhNTaM}R2D(t}oHb0$XdmWie zn0U-RI1QGOw`zEa!oL#uRQqJ>IcpbQ?sl#)Be@S`mmCOOaBI%_yY&kD7+$|b`or(V zZx#%gP5KQ<mS*X%>+>g4h!Js>{ zKLd4(JJGf&K6WJMI;t;Y7A_qLOSS10i63RV-SxJZ3njodX*baa0$7Mi550^mlC7g& zEwzZarUDhL}{uia6>L+5&Ln3>4nI3}&rJvZlt>t)qI{}gyDj+9wr=^f+x*q?s;kYuNuHfn|i#Kem- z!OML_VDXsl7Y@o)OI)-WfhDU{P2y4 z$U}7Cn$F1VI|WVM4L^Z>(cnwmmN-%1b}@#Hnuoi^1e1LdY7rRZK#!9=72=dih0Yeci-|)kHO1T+zyMa8!*dM%Zj_dl)%!a~tn^Kf8jY2pRh6mlfDc zrw&l#gtfxzQ0WHnBH*FwFZo$D$JwU9WEGsoQ96x<+gN$b5%K}{gi$~gd+Uz)vkV>= zoI=LcJY!b5Xv)&*M~Nx4e57-GfK_?~#g%*ka?Gy^tLQ}JhBo0gwrFzc-1EhNbeg2* z&5}oGr=h7(CD$D&7)D+U&l95EA1G5m{79+tQ!isc_oX#PWpH&SgNe|FbC)3h<}bds zF~~VB?$Th#Hv9uNMSqvcE-%<+UIN4wUsFCcCE3w+k$)1c@xMj;ycYt>R|AiI-meZk zk|M`LS-O16MMH-gh$&Fs?eqD=fh^mYW&4!m@>{mdxnIa%F5N*U`^aSfRG8$ezvUf| zdAH2Np9{!Et#iSAgn*(|?cv5Uh>AmNr1hU2;bgD`K!TWo4Z3QpYM||XBttGcLQ9aG z;%*A2wXy;i6_t%sC%y997=JrON-gLG%!PHA1lTm#wf zvrMweHC@qR(Yc>D)J>W3B^R9^u;zqLRQP27t^Cl5GUG7xX7T@-eV9-7vW!;ZW|QFU z;3Y-Auz!~({?0`NFaraCbxorT6H=#(fxvO1av;j?Os5J@eGNCtj-Cfb|r~tqI$W4DLV5|fADzj!5QmbW?WbY7imy^UKfk^>x6|!dD3e!wt<6kjo zI9i&C&lpq@I6kxZ5p}9a>YK0`RiVaB24Z9`*~^<1N&N{*la|#ORV=R2Ff@YgNDPlJ z7VX>;GYd!x5(+_FRfyf+hWJDRW&&bFZ{AQ6m|&>s0^KKi1>biPR?AUWB5G+MD@T|j z%c+T&v$Vu3wD)2HydBL%%br1&PE#5`(IP3C{$O<#K~5~(T~W+u&&E1Z8<0C*PDx`G zE&dpJaXY$kbCr%JM@t@}U_Q%ztI1PHCP`vW5klLD?kp=out^0Oze@#YA=svZrAa9z zM0$aM0E@O~cKC&Bi(vn1Xf`P%ZrwX&8rnupgbr+!GKf`3Yno+Uxq_&yf(SwNJsxmb*>B>^&PEqBko~hwZ^_<}zFqq*P z(x zGMVDU+TX?UIr4Pd1zMSB5Jp1jGZ>z<&ma;MoHx^Fs1{iu9)m3IlE{JnpGh4|p<_{n zd29#=(r5c$xEXUF8}knGwgD^Xi*mY-vJ8%_eraEn_Ce_#k>X^@I5#xZ9x#D3CUDjS z22J3+2@ILQITIMoP#pg58`(#lQnK`VGUI5#%97NFE5AXt4s{Xioj60+e#%Iy0hH6? zhk~+e_lgR!HdKgxg9>p}{7tmGXp9`_ahb7c8#+NDyCiKsX+J$r9W<;~oO!fy19sWchdrN0u zGR5pOev7F|D*DO%CH27otLBtm-YpHYM$TC{9m&j&CXvn`e>@;6peB>SL}>GZ8Td<}e6p&YskT|L z7aKR&;ILstfBGtS(_;_hC!td2#%JH<$5&ZedrX$z8fk74>oT4fx?YYUO_q#e-rJ{BOoL6hF5NUNNp%7LmZzx?~Q$Gh@xvHbxyv>z5T=T5f6 zZpc}4VqC!ET4S(HjeRsws-VC^w1O$iz_f0S!7PJl#S_+ul44XJ)z*VbzC*&hKw7!S zCLa<$5-6v3&_ZlI6x$JqO><(7W=|rv6CVkPEvO73T0wdI9QCKg7R)k;_T2!#A?K@n zD+P=ju?6LC2BTaf=>?J&C&wUKCdueK-Y6pPi`$vm%=->R!=fNquzXS6=c`lB(Dz5> zF6(gS8$z6l&E`iCH?K9mqk(7un(41zg+H*YeYK6)^~6Ov)a zJky@TkKR-Lh{#;MR}x=6V=P{~pNXYtO{_%pDS%EsLofZ(ipOM!w~7>}7LfxtTx6wq z0u1W;Pato1Q@-19oTFcK;09*y|i+B$lmVo=2FrqxFc#%220T{=7b1q>f3(A;! zT-t;EB4bwKC^njyqr9V)IMsR9Nu0qK?HrsawNEDpugA9rUHoAK*op6-bnt>Eh5;!U zNcI&uHiBmy`#h>lHc1WLY@dg9gMBdE=9Kx=;9uvMi&R}eE^_YS7e6QQi*5~fDg|v6 zcyDD%+aBYquFhkx&w2lZMMq8eztV`48pB%k+Zr(ZsH880xF&!+KZ+kYz^qh z0XM-7E)uyBk(sw|by7E+)^=!28`}CVkaarZ`1NlR!Kdlc_Ve`artLmzQ`$ZP6uf8d zsCqi4Zt^tzW~iNhC;IR&5#iPx)pJApSg;z@+(*^KDGx2#daPh?33lD&G-}DEuE2gT z84`fHa^(c))p+$CjtCEj>37G1UD!M_LKbv z@=n@Ep+ZM1Q$Z7nL)phL+ND{^3g zD7a|$@XvuXagNy0i5;mENxHIFcJNJzQi6^zZ9l{6Wv`K~R#lt99%hJW&}}^z>%bYM zj>Mr@huxX^Xwvtu^i#;{iom>@tpaaO#17Zw*k#kTWAoY&Uik7WR5wM(uJ_rrt#-#mzwjy_cw$hY&+~k zCy?vi4f>P3*wv~xo2KnSlmcUbY&{)*O~wNO4W~t`uvrwV9y#7w#?7qI%R#S-Qu0NR znsa6l84M00_+YOZLAVQ5J4advWds?_HG+6)E~f4CidUIfr0r2Y4Ijo2hGJA>WdA{g zHGCw2?V5%S*ed~(z_2mq;b0@DurWF*8?X^Bz=BH5Ks~|g;I`fK>~3kDjA9%)Z&6c5 zF`9e`y^=g5W=O?M7fzp#9H33WxcCO_Je@?uQr(m>3Pkl7$_Iax_~%4D29ft5__<9+ zw{vJ1*w+mn-i+tW_nJ58w~yfK$xhYL72A)pgzC5Xf{swC!2BbB8k6ETPUa5Mm)yGA8cmkjeT8pb^_k#QVk0B06EQ^c|Xn|^H#sRzz*oxnzmTs!$u-Z)lyIok#;`Sb*d;Waq|isM5Ed{*Bp+})-4 zbL>M3j1P_`B_0xE$uR;b#xqAG|1Z8e}>!D$T8%_8;p?^xViSBd}>3D^gUQw#88d1jBYhY z{$se$!W}+Y?B#H3`fxA99V2>mIqn0vFTfq0Z8bU)p6jcQ;XVXsSv9%{_@460ogd2E zgwvBRsnZoN#R^{GY048n!FHsV;m=jwj`zuhTzgLL2amB*>_GvKu`=AjW2_u^@EBWw zJ9vy$;tn2TG3-$RkJZ(j^W~whuuXbm?A#$8SSPq2!=2B^ai4{IAMVH<(s3XqaaBS} zFedPf+-cfCN-l>fNC~f{$Gv(4I}IANo~bYH;ynSX573HSA)nIZH&+BNl6pHvMg5RD z<`}^YH=A)Ii7TWTaYVJj@>@| zKD04@yI>-TOUI?U#KA)EeFyg8Wb0u}4&TC7s^1RJ11$Ny9sWq#?v(!0K17B_=211# zHG7*fq~B6Ar=ze3E9|43cF-Rv(~d(h@X?@i_?E%?E0I;oqH@RoS$L8>4eh{Z$ic-&N>-Y!xYW?XMNl1@Pd8My)scU3@p`- z{d|5RM;`cSfwy(f?#nNk$^#xtnBXW@ux`EWee9GtOTk+y;XW}IsPNAD0-8GJ3(BQo zrVKF`Tgx0YcNbgB9BeUjv9-*>7Bg(Y!ypA_qPWkk-QC zAO99wxBv5^X#HRtbh@bsd5QyOC0@A>_cC0}k*`_U1^FCR50jUl5QJ!a{)b@hr6u^M z&V{Q^>Zro;Q3LQsrtKc!f&(lWpiNLWdzgA;j(~7dR6ti4jL`Wq zPHPY|sG1(k5S&-x1bp)cV0wZxSrb)TleT^(%la0abYw=0Y4|QY=x&weEf`Q`qA#@u zGdV^3>pREMeq)gK1+E5-vFGH9&=3R!2tIcV$9y#^g{PT#SBI@ML`LGFo@S`SSOGjq zQz<+e${Du?v%#Lpl^9q9|2;~q9h{s;c0-$=vEuM;bVH|~qc4Ha-nPy4+52rz@!9{v z=RgP5i{H9!TjTb++iDx)b&ZLZ+qTy~@kG3(sdiJ{tu0Ts+}2RPF$3{M&fR3yKUmis zzh`lI>5Vs*N_f$H3Eqpe09#fGWQwb)K33bZZCPVtNBs8p)i0}SXn1Uq52isOyeA$j zUA(x|kM_i4R$2Y(_4PY$tzU`Xo9-#M>Q~;gSRN$bm5UZjyj8#24_$hyN>L1v$rKK)jx@d7}0I}YPc+nkJJ@YZolIIF zvuW*`^^J)*C`tv^H7~kd)v9Hcl{HDxw=}lAZ_8tI>+fx--Fo8#t5?{o)~>H!ZLhIc zKe%GmeK*~(xcnxmOU>HG2b$}_Q8gKB-dWdNzhkR4E|ZmgJynvb z+9&FSm?>E3eqY^=y5`z=-I{H+&2^ip^!1xqzo?3p^(}F$s;+r!9lv>duMIKTIm54+ItiAo1R$H*lddCJ=>eN0^M3(x3z9Zb#r4wwiW(AAvJ}+22Q2RO0ys0(@7SFKk)&EnD`U(z2S3=ui@+cg?+hZuzZu zCgf?}?Kdev>Fwq7?wVI#ekT&n)QJl|xY{b%6)m`K`jsU#@n!?ge_q@9v*bG)0l_`=7%^y_r|nn3(2FMhX< zpX-mW&(MEH$A6mkh{?a`6QT-J}pq%&X_=O02dBi2T#2?o2YgiV;W6>&+V<|wV zbbQA(zIgJ`c8$lwkjD8ix6FAoQdb)uN^ro7*N6FI;tNw=`Z68=*?=6L5jn67F&%Hi zJLIJ=s(gc?zfH$q8z{p}PZ!#C{6pl^HkQ>rOyN`rs3sUzof4O>^=>DSi?8r*<3{yDOYi6OZor=3s4_~uha#VwhK5vOGfyY z+7-U&D}vVpPB}cRz@8?RY5dKa|0W%Or-n!0tpL);BKU}=WBRpHjZfDL^L>?ygiHE# z-1!wj!k52Gz?Sso2+~F%ygwwLPigoL>CAM$3j8ksPX0}Qi5J7j^@4_P(Dge04F~Knm*p{BNxXs z;`dP@ap_`M_iOksHC*~)#6PI%nErLTq_V6=4L21P)o_0QnDqaZ3XDtoQ`|qQ;hQx4 z!xC#*{QM;Ge^jRc(tjiP7n*)l>p6#bUTF<4)o`uO)}W>{+N$uRpGELz8oq3wf=mC3 z;4d_ta!tq7R}}J+dem_CU%YP8@ahnpaYYtp+*z(7GYJmG6?}g0q|>3f*a3M@JGRERt$BZfGd2BWt!ixEUfWRr!8&W_=Ek`7 zAf|th)-^X;nR(xO&R5s2ePsQ;HLI4bUGc!G_3P)}F>k@0mQ3>2HZHG?*IG+LZQZVAjg8Hlt*pf6m*0^i;_mY75qHj; zXDzF3*aW&un(JznFjm#UJF~H|xkd7qd9DYNxp&sg@0x$7wPZ8B2WuO}2VwbV!r7DD zeS3Dc?!Ns_%bWjN%Nn;gLDMqc65rH0E8V*n+?}27f_Zmj=&E@&gvvm6{DrsQl_TP= z>^aOYza5gkZ{-6^m#kd>z`ggbvDdC&yJYD~d%e~2{^s}=P^)c$Lngj{OKp9Fg&F-? z__4$zu)JG5mO-|}MjxfuUgosx^=0x#a!2d~rpEBixU2Hv3ID?{+z8vf%D{F0{oFJZXO_vaz_ ze3hN`pBhfG_u${~xmlZZhMzW_ubTrtqWf8czhC3O?t@qQ=nv~5#NeCx!heL|hE8D# z65}G-CHOadUULKyRzYJ)S({|`d&10;yc*7kjv{^LHl z4Ll>~kG@5cjnlU;<$(9)fd4QDeA-(x^!;-F{T%S}9Pnj1;D6$SFZY$}i#g!4u2uXP zdCo3U&vQa>BcGc?@O28^D%Wtz+26mt_4l&Ve|rdS=-(8A@7MI_YIvCbDj(c0&+qx* zOF_fXKdR?xq~pgwo&$a=2Yet0{I9MLme=&>54|G@U!>){DFios{!s}21(n=t)bOwz zz7oPW^uHd08~Wc2!M~>Ie=7(5N9Sbce@h5%=x-0fzpLptYj`o9{d!XH&g}HB4#5rm znIZUjP5)XA57Tc7;T!qv4#5rmWC%VJ^JHG_Iq2Vgql!1}$k1OFf*bnxhv2`b=|7<1 zVgB#Yegwj8{2MyGw*>QDSEQa_48aZlKnOlh=lcT<59j*?^1OS!@s;N2sifO zA1n{T&G@n+1ULNr_+(67+&F#UZYd?TNZ z5Zur|6oMx;|DVi3{~PZO@_$~-KOKS_`riw|KdtG1U&F)vx35+73HR&6=W@WmnFD^w z2lwmGTWT`&{qVUt;DZka%X>+ecQg#w^P1N~@Gt7}j%j$fyh|R+PQNM)*Ywwh;AV~P z5e*O1FI%6T{`@dp)2|4@f2#Sf)bKF2j7(gey@fz-}~@y_*@YW(x0u%RTF|6{B<#97 zi_Ujf2yXJ-8-kmB_iK1K-^KeB{cyh9Q^9-}>3p9I!A-txA-Gx7Pic5K-xYe@%haQx zUlW2G`s+gQ6GomI9;UxDgm36S6@nZ3P6+-(P5)pH`k(W`ZHUUynR_rOx0PCM?+L*T z{*n-UMCW_2hKI{ls>q97q)`P@8G>)u&l^JUf78!xA-Gvj?g_!odU9U~Zq}0rLvXX6 zjHMU3$Rq#FdU9z9Zq}14LvXX6ydeZP>&9&%xLF_W3BgUh_l4kQU1%@_H|s*ewC0`s zn|0SxEp$^Zvz}ZTf}8c#4I#K$Uu_G)&H8Om2yWJ~Ple!Sz509zZq{jwi@-E4M)TjS z)6NOO&HC!{5ZtV9ZwtZA`gVH=Zq`wcgy3c!wLb(m>*1pzxLN-+C(} z&20SHW1w{88F zW^7z#-Trr6r69s4@P-hvO8vY+_d_OHHy870U55}wQ0YNbpyYGvUO7|Uxo*w0CF;?d zrSEhf5U$lgLoKT(l&^^h=YI<#Nb|Q%KM!e1lltMmO#a7o{hGK^6VP`~@0q?l1h4*> z0?ZA;4gco86!9kPN8O{-`(X@+aXH-}EX;qKPG52bQ)MnAe*;;KK(_jeeO+Og_RRWY zT9e+?-weRQvS-Q*&+*r?YSqpA4fCIkcOtiUt0a9|F{k5k5&pyZb9p9P{;@w(>3c(w H;r#zEDFCW8 literal 0 HcmV?d00001 diff --git a/test-data/mikktspace.h b/test-data/mikktspace.h new file mode 100644 index 0000000000..52c44a713c --- /dev/null +++ b/test-data/mikktspace.h @@ -0,0 +1,145 @@ +/** \file mikktspace/mikktspace.h + * \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. + */ + +#ifndef __MIKKTSPACE_H__ +#define __MIKKTSPACE_H__ + + +#ifdef __cplusplus +extern "C" { +#endif + +/* Author: Morten S. Mikkelsen + * Version: 1.0 + * + * The files mikktspace.h and mikktspace.c are designed to be + * stand-alone files and it is important that they are kept this way. + * Not having dependencies on structures/classes/libraries specific + * to the program, in which they are used, allows them to be copied + * and used as is into any tool, program or plugin. + * The code is designed to consistently generate the same + * tangent spaces, for a given mesh, in any tool in which it is used. + * This is done by performing an internal welding step and subsequently an order-independent evaluation + * of tangent space for meshes consisting of triangles and quads. + * This means faces can be received in any order and the same is true for + * the order of vertices of each face. The generated result will not be affected + * by such reordering. Additionally, whether degenerate (vertices or texture coordinates) + * primitives are present or not will not affect the generated results either. + * Once tangent space calculation is done the vertices of degenerate primitives will simply + * inherit tangent space from neighboring non degenerate primitives. + * The analysis behind this implementation can be found in my master's thesis + * which is available for download --> 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