a12a8a71bb
The goal is to solve confusion of the "All rights reserved" for licensing code under an open-source license. The phrase "All rights reserved" comes from a historical convention that required this phrase for the copyright protection to apply. This convention is no longer relevant. However, even though the phrase has no meaning in establishing the copyright it has not lost meaning in terms of licensing. This change makes it so code under the Blender Foundation copyright does not use "all rights reserved". This is also how the GPL license itself states how to apply it to the source code: <one line to give the program's name and a brief idea of what it does.> Copyright (C) <year> <name of author> This program is free software ... This change does not change copyright notice in cases when the copyright is dual (BF and an author), or just an author of the code. It also does mot change copyright which is inherited from NaN Holding BV as it needs some further investigation about what is the proper way to handle it.
825 lines
25 KiB
C++
825 lines
25 KiB
C++
/* SPDX-License-Identifier: Apache-2.0
|
|
*
|
|
* Original C code:
|
|
* Copyright 2011 by Morten S. Mikkelsen.
|
|
*
|
|
* C++ rewrite:
|
|
* Copyright 2022 Blender Foundation
|
|
*/
|
|
|
|
/** \file
|
|
* \ingroup mikktspace
|
|
*/
|
|
|
|
#include <algorithm>
|
|
#include <cassert>
|
|
|
|
#ifdef WITH_TBB
|
|
# include <tbb/parallel_for.h>
|
|
#endif
|
|
|
|
#include "mikk_atomic_hash_set.hh"
|
|
#include "mikk_float3.hh"
|
|
#include "mikk_util.hh"
|
|
|
|
namespace mikk {
|
|
|
|
static constexpr uint UNSET_ENTRY = 0xffffffffu;
|
|
|
|
template<typename Mesh> class Mikktspace {
|
|
struct Triangle {
|
|
/* Stores neighboring triangle for group assignment. */
|
|
std::array<uint, 3> neighbor;
|
|
/* Stores assigned group of each vertex. */
|
|
std::array<uint, 3> group;
|
|
/* Stores vertex indices that make up the triangle. */
|
|
std::array<uint, 3> vertices;
|
|
|
|
/* Computed face tangent, will be accumulated into group. */
|
|
float3 tangent;
|
|
|
|
/* Index of the face that this triangle belongs to. */
|
|
uint faceIdx;
|
|
/* Index of the first of this triangle's vertices' TSpaces. */
|
|
uint tSpaceIdx;
|
|
|
|
/* Stores mapping from this triangle's vertices to the original
|
|
* face's vertices (relevant for quads). */
|
|
std::array<uint8_t, 3> faceVertex;
|
|
|
|
// flags
|
|
bool markDegenerate : 1;
|
|
bool quadOneDegenTri : 1;
|
|
bool groupWithAny : 1;
|
|
bool orientPreserving : 1;
|
|
|
|
Triangle(uint faceIdx_, uint tSpaceIdx_)
|
|
: tangent{0.0f},
|
|
faceIdx{faceIdx_},
|
|
tSpaceIdx{tSpaceIdx_},
|
|
markDegenerate{false},
|
|
quadOneDegenTri{false},
|
|
groupWithAny{true},
|
|
orientPreserving{false}
|
|
{
|
|
neighbor.fill(UNSET_ENTRY);
|
|
group.fill(UNSET_ENTRY);
|
|
}
|
|
|
|
void setVertices(uint8_t i0, uint8_t i1, uint8_t i2)
|
|
{
|
|
faceVertex[0] = i0;
|
|
faceVertex[1] = i1;
|
|
faceVertex[2] = i2;
|
|
vertices[0] = pack_index(faceIdx, i0);
|
|
vertices[1] = pack_index(faceIdx, i1);
|
|
vertices[2] = pack_index(faceIdx, i2);
|
|
}
|
|
};
|
|
|
|
struct Group {
|
|
float3 tangent;
|
|
uint vertexRepresentative;
|
|
bool orientPreserving;
|
|
|
|
Group(uint vertexRepresentative_, bool orientPreserving_)
|
|
: tangent{0.0f},
|
|
vertexRepresentative{vertexRepresentative_},
|
|
orientPreserving{orientPreserving_}
|
|
{
|
|
}
|
|
|
|
void normalizeTSpace()
|
|
{
|
|
tangent = tangent.normalize();
|
|
}
|
|
|
|
void accumulateTSpaceAtomic(float3 v_tangent)
|
|
{
|
|
float_add_atomic(&tangent.x, v_tangent.x);
|
|
float_add_atomic(&tangent.y, v_tangent.y);
|
|
float_add_atomic(&tangent.z, v_tangent.z);
|
|
}
|
|
|
|
void accumulateTSpace(float3 v_tangent)
|
|
{
|
|
tangent += v_tangent;
|
|
}
|
|
};
|
|
|
|
struct TSpace {
|
|
float3 tangent = float3(1.0f, 0.0f, 0.0f);
|
|
uint counter = 0;
|
|
bool orientPreserving = false;
|
|
|
|
void accumulateGroup(const Group &group)
|
|
{
|
|
assert(counter < 2);
|
|
|
|
if (counter == 0) {
|
|
tangent = group.tangent;
|
|
}
|
|
else if (tangent == group.tangent) {
|
|
// 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, so do nothing
|
|
}
|
|
else {
|
|
tangent = (tangent + group.tangent).normalize();
|
|
}
|
|
|
|
counter++;
|
|
orientPreserving = group.orientPreserving;
|
|
}
|
|
};
|
|
|
|
Mesh &mesh;
|
|
|
|
std::vector<Triangle> triangles;
|
|
std::vector<TSpace> tSpaces;
|
|
std::vector<Group> groups;
|
|
|
|
uint nrTSpaces, nrFaces, nrTriangles, totalTriangles;
|
|
|
|
int nrThreads;
|
|
bool isParallel;
|
|
|
|
public:
|
|
Mikktspace(Mesh &mesh_) : mesh(mesh_) {}
|
|
|
|
void genTangSpace()
|
|
{
|
|
nrFaces = uint(mesh.GetNumFaces());
|
|
|
|
#ifdef WITH_TBB
|
|
nrThreads = tbb::this_task_arena::max_concurrency();
|
|
isParallel = (nrThreads > 1) && (nrFaces > 10000);
|
|
#else
|
|
nrThreads = 1;
|
|
isParallel = false;
|
|
#endif
|
|
|
|
// make an initial triangle --> face index list
|
|
generateInitialVerticesIndexList();
|
|
|
|
if (nrTriangles == 0) {
|
|
return;
|
|
}
|
|
|
|
// make a welded index list of identical positions and attributes (pos, norm, texc)
|
|
generateSharedVerticesIndexList();
|
|
|
|
// mark all triangle pairs that belong to a quad with only one
|
|
// good triangle. These need special treatment in degenEpilogue().
|
|
// Additionally, move all good triangles to the start of
|
|
// triangles[] without changing order and
|
|
// put the degenerate triangles last.
|
|
degenPrologue();
|
|
|
|
if (nrTriangles == 0) {
|
|
// No point in building tangents if there are no non-degenerate triangles, so just zero them
|
|
tSpaces.resize(nrTSpaces);
|
|
}
|
|
else {
|
|
// evaluate triangle level attributes and neighbor list
|
|
initTriangle();
|
|
|
|
// match up edge pairs
|
|
buildNeighbors();
|
|
|
|
// based on the 4 rules, identify groups based on connectivity
|
|
build4RuleGroups();
|
|
|
|
// make tspaces, each group is split up into subgroups.
|
|
// Finally a tangent space is made for every resulting subgroup
|
|
generateTSpaces();
|
|
|
|
// degenerate quads with one good triangle will be fixed by copying a space from
|
|
// the good triangle to the coinciding vertex.
|
|
// all other degenerate triangles will just copy a space from any good triangle
|
|
// with the same welded index in vertices[].
|
|
degenEpilogue();
|
|
}
|
|
|
|
uint index = 0;
|
|
for (uint f = 0; f < nrFaces; f++) {
|
|
const uint verts = mesh.GetNumVerticesOfFace(f);
|
|
if (verts != 3 && verts != 4) {
|
|
continue;
|
|
}
|
|
|
|
// set data
|
|
for (uint i = 0; i < verts; i++) {
|
|
const TSpace &tSpace = tSpaces[index++];
|
|
mesh.SetTangentSpace(f, i, tSpace.tangent, tSpace.orientPreserving);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected:
|
|
template<typename F> void runParallel(uint start, uint end, F func)
|
|
{
|
|
#ifdef WITH_TBB
|
|
if (isParallel) {
|
|
tbb::parallel_for(start, end, func);
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
for (uint i = start; i < end; i++) {
|
|
func(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
float3 getPosition(uint vertexID)
|
|
{
|
|
uint f, v;
|
|
unpack_index(f, v, vertexID);
|
|
return mesh.GetPosition(f, v);
|
|
}
|
|
|
|
float3 getNormal(uint vertexID)
|
|
{
|
|
uint f, v;
|
|
unpack_index(f, v, vertexID);
|
|
return mesh.GetNormal(f, v);
|
|
}
|
|
|
|
float3 getTexCoord(uint vertexID)
|
|
{
|
|
uint f, v;
|
|
unpack_index(f, v, vertexID);
|
|
return mesh.GetTexCoord(f, v);
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
void generateInitialVerticesIndexList()
|
|
{
|
|
nrTriangles = 0;
|
|
for (uint f = 0; f < nrFaces; f++) {
|
|
const uint verts = mesh.GetNumVerticesOfFace(f);
|
|
if (verts == 3) {
|
|
nrTriangles += 1;
|
|
}
|
|
else if (verts == 4) {
|
|
nrTriangles += 2;
|
|
}
|
|
}
|
|
|
|
triangles.reserve(nrTriangles);
|
|
|
|
nrTSpaces = 0;
|
|
for (uint f = 0; f < nrFaces; f++) {
|
|
const uint verts = mesh.GetNumVerticesOfFace(f);
|
|
if (verts != 3 && verts != 4)
|
|
continue;
|
|
|
|
uint tA = uint(triangles.size());
|
|
triangles.emplace_back(f, nrTSpaces);
|
|
Triangle &triA = triangles[tA];
|
|
|
|
if (verts == 3) {
|
|
triA.setVertices(0, 1, 2);
|
|
}
|
|
else {
|
|
uint tB = uint(triangles.size());
|
|
triangles.emplace_back(f, nrTSpaces);
|
|
Triangle &triB = triangles[tB];
|
|
|
|
// need an order independent way to evaluate
|
|
// tspace on quads. This is done by splitting
|
|
// along the shortest diagonal.
|
|
float distSQ_02 = (mesh.GetTexCoord(f, 2) - mesh.GetTexCoord(f, 0)).length_squared();
|
|
float distSQ_13 = (mesh.GetTexCoord(f, 3) - mesh.GetTexCoord(f, 1)).length_squared();
|
|
bool quadDiagIs_02;
|
|
if (distSQ_02 != distSQ_13)
|
|
quadDiagIs_02 = (distSQ_02 < distSQ_13);
|
|
else {
|
|
distSQ_02 = (mesh.GetPosition(f, 2) - mesh.GetPosition(f, 0)).length_squared();
|
|
distSQ_13 = (mesh.GetPosition(f, 3) - mesh.GetPosition(f, 1)).length_squared();
|
|
quadDiagIs_02 = !(distSQ_13 < distSQ_02);
|
|
}
|
|
|
|
if (quadDiagIs_02) {
|
|
triA.setVertices(0, 1, 2);
|
|
triB.setVertices(0, 2, 3);
|
|
}
|
|
else {
|
|
triA.setVertices(0, 1, 3);
|
|
triB.setVertices(1, 2, 3);
|
|
}
|
|
}
|
|
|
|
nrTSpaces += verts;
|
|
}
|
|
}
|
|
|
|
struct VertexHash {
|
|
Mikktspace<Mesh> *mikk;
|
|
inline uint operator()(const uint &k) const
|
|
{
|
|
return hash_float3x3(mikk->getPosition(k), mikk->getNormal(k), mikk->getTexCoord(k));
|
|
}
|
|
};
|
|
|
|
struct VertexEqual {
|
|
Mikktspace<Mesh> *mikk;
|
|
inline bool operator()(const uint &kA, const uint &kB) const
|
|
{
|
|
return mikk->getTexCoord(kA) == mikk->getTexCoord(kB) &&
|
|
mikk->getNormal(kA) == mikk->getNormal(kB) &&
|
|
mikk->getPosition(kA) == mikk->getPosition(kB);
|
|
}
|
|
};
|
|
|
|
/* Merge identical vertices.
|
|
* To find vertices with identical position, normal and texcoord, we calculate a hash of the 9
|
|
* values. Then, by sorting based on that hash, identical elements (having identical hashes) will
|
|
* be moved next to each other. Since there might be hash collisions, the elements of each block
|
|
* are then compared with each other and duplicates are merged.
|
|
*/
|
|
template<bool isAtomic> void generateSharedVerticesIndexList_impl()
|
|
{
|
|
uint numVertices = nrTriangles * 3;
|
|
AtomicHashSet<uint, isAtomic, VertexHash, VertexEqual> set(numVertices, {this}, {this});
|
|
runParallel(0u, nrTriangles, [&](uint t) {
|
|
for (uint i = 0; i < 3; i++) {
|
|
auto res = set.emplace(triangles[t].vertices[i]);
|
|
if (!res.second) {
|
|
triangles[t].vertices[i] = res.first;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
void generateSharedVerticesIndexList()
|
|
{
|
|
if (isParallel) {
|
|
generateSharedVerticesIndexList_impl<true>();
|
|
}
|
|
else {
|
|
generateSharedVerticesIndexList_impl<false>();
|
|
}
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
|
/////////////////////////////////// Degenerate triangles ////////////////////////////////////
|
|
|
|
void degenPrologue()
|
|
{
|
|
// Mark all degenerate triangles
|
|
totalTriangles = nrTriangles;
|
|
std::atomic<uint> degenTriangles(0);
|
|
runParallel(0u, totalTriangles, [&](uint t) {
|
|
const float3 p0 = getPosition(triangles[t].vertices[0]);
|
|
const float3 p1 = getPosition(triangles[t].vertices[1]);
|
|
const float3 p2 = getPosition(triangles[t].vertices[2]);
|
|
if (p0 == p1 || p0 == p2 || p1 == p2) // degenerate
|
|
{
|
|
triangles[t].markDegenerate = true;
|
|
degenTriangles.fetch_add(1);
|
|
}
|
|
});
|
|
nrTriangles -= degenTriangles.load();
|
|
|
|
if (totalTriangles == nrTriangles) {
|
|
return;
|
|
}
|
|
|
|
// locate quads with only one good triangle
|
|
runParallel(0u, totalTriangles - 1, [&](uint t) {
|
|
Triangle &triangleA = triangles[t], &triangleB = triangles[t + 1];
|
|
if (triangleA.faceIdx != triangleB.faceIdx) {
|
|
/* Individual triangle, skip. */
|
|
return;
|
|
}
|
|
if (triangleA.markDegenerate != triangleB.markDegenerate) {
|
|
triangleA.quadOneDegenTri = true;
|
|
triangleB.quadOneDegenTri = true;
|
|
}
|
|
});
|
|
|
|
std::stable_partition(triangles.begin(), triangles.end(), [](const Triangle &tri) {
|
|
return !tri.markDegenerate;
|
|
});
|
|
}
|
|
|
|
void degenEpilogue()
|
|
{
|
|
if (nrTriangles == totalTriangles) {
|
|
return;
|
|
}
|
|
|
|
std::unordered_map<uint, uint> goodTriangleMap;
|
|
for (uint t = 0; t < nrTriangles; t++) {
|
|
for (uint i = 0; i < 3; i++) {
|
|
goodTriangleMap.emplace(triangles[t].vertices[i], pack_index(t, i));
|
|
}
|
|
}
|
|
|
|
// deal with degenerate triangles
|
|
// punishment for degenerate triangles is O(nrTriangles) extra memory.
|
|
for (uint t = nrTriangles; t < totalTriangles; t++) {
|
|
// degenerate triangles on a quad with one good triangle are skipped
|
|
// here but processed in the next loop
|
|
if (triangles[t].quadOneDegenTri) {
|
|
continue;
|
|
}
|
|
|
|
for (uint i = 0; i < 3; i++) {
|
|
const auto entry = goodTriangleMap.find(triangles[t].vertices[i]);
|
|
if (entry == goodTriangleMap.end()) {
|
|
// Matching vertex from good triangle is not found.
|
|
continue;
|
|
}
|
|
|
|
uint tSrc, iSrc;
|
|
unpack_index(tSrc, iSrc, entry->second);
|
|
const uint iSrcVert = triangles[tSrc].faceVertex[iSrc];
|
|
const uint iSrcOffs = triangles[tSrc].tSpaceIdx;
|
|
const uint iDstVert = triangles[t].faceVertex[i];
|
|
const uint iDstOffs = triangles[t].tSpaceIdx;
|
|
// copy tspace
|
|
tSpaces[iDstOffs + iDstVert] = tSpaces[iSrcOffs + iSrcVert];
|
|
}
|
|
}
|
|
|
|
// deal with degenerate quads with one good triangle
|
|
for (uint t = 0; t < nrTriangles; t++) {
|
|
// this triangle belongs to a quad where the
|
|
// other triangle is degenerate
|
|
if (!triangles[t].quadOneDegenTri) {
|
|
continue;
|
|
}
|
|
uint vertFlag = (1u << triangles[t].faceVertex[0]) | (1u << triangles[t].faceVertex[1]) |
|
|
(1u << triangles[t].faceVertex[2]);
|
|
uint missingFaceVertex = 0;
|
|
if ((vertFlag & 2) == 0)
|
|
missingFaceVertex = 1;
|
|
else if ((vertFlag & 4) == 0)
|
|
missingFaceVertex = 2;
|
|
else if ((vertFlag & 8) == 0)
|
|
missingFaceVertex = 3;
|
|
|
|
uint faceIdx = triangles[t].faceIdx;
|
|
float3 dstP = mesh.GetPosition(faceIdx, missingFaceVertex);
|
|
bool found = false;
|
|
for (uint i = 0; i < 3; i++) {
|
|
const uint faceVertex = triangles[t].faceVertex[i];
|
|
const float3 srcP = mesh.GetPosition(faceIdx, faceVertex);
|
|
if (srcP == dstP) {
|
|
const uint offset = triangles[t].tSpaceIdx;
|
|
tSpaces[offset + missingFaceVertex] = tSpaces[offset + faceVertex];
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
assert(found);
|
|
(void)found;
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// returns the texture area times 2
|
|
float calcTexArea(uint tri)
|
|
{
|
|
const float3 t1 = getTexCoord(triangles[tri].vertices[0]);
|
|
const float3 t2 = getTexCoord(triangles[tri].vertices[1]);
|
|
const float3 t3 = getTexCoord(triangles[tri].vertices[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 signedAreaSTx2 = t21x * t31y - t21y * t31x;
|
|
return fabsf(signedAreaSTx2);
|
|
}
|
|
|
|
void initTriangle()
|
|
{
|
|
// triangles[f].iFlag is cleared in generateInitialVerticesIndexList()
|
|
// which is called before this function.
|
|
|
|
// evaluate first order derivatives
|
|
runParallel(0u, nrTriangles, [&](uint t) {
|
|
Triangle &triangle = triangles[t];
|
|
|
|
// initial values
|
|
const float3 v1 = getPosition(triangle.vertices[0]);
|
|
const float3 v2 = getPosition(triangle.vertices[1]);
|
|
const float3 v3 = getPosition(triangle.vertices[2]);
|
|
const float3 t1 = getTexCoord(triangle.vertices[0]);
|
|
const float3 t2 = getTexCoord(triangle.vertices[1]);
|
|
const float3 t3 = getTexCoord(triangle.vertices[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 float3 d1 = v2 - v1, d2 = v3 - v1;
|
|
|
|
const float signedAreaSTx2 = t21x * t31y - t21y * t31x;
|
|
const float3 vOs = (t31y * d1) - (t21y * d2); // eq 18
|
|
const float3 vOt = (-t31x * d1) + (t21x * d2); // eq 19
|
|
|
|
triangle.orientPreserving = (signedAreaSTx2 > 0);
|
|
|
|
if (not_zero(signedAreaSTx2)) {
|
|
const float lenOs2 = vOs.length_squared();
|
|
const float lenOt2 = vOt.length_squared();
|
|
const float fS = triangle.orientPreserving ? 1.0f : (-1.0f);
|
|
if (not_zero(lenOs2))
|
|
triangle.tangent = vOs * (fS / sqrtf(lenOs2));
|
|
|
|
// if this is a good triangle
|
|
if (not_zero(lenOs2) && not_zero(lenOt2))
|
|
triangle.groupWithAny = false;
|
|
}
|
|
});
|
|
|
|
// force otherwise healthy quads to a fixed orientation
|
|
runParallel(0u, nrTriangles - 1, [&](uint t) {
|
|
Triangle &triangleA = triangles[t], &triangleB = triangles[t + 1];
|
|
if (triangleA.faceIdx != triangleB.faceIdx) {
|
|
// this is not a quad
|
|
return;
|
|
}
|
|
|
|
// bad triangles should already have been removed by
|
|
// degenPrologue(), but just in case check that neither are degenerate
|
|
if (!(triangleA.markDegenerate || triangleB.markDegenerate)) {
|
|
// if this happens the quad has extremely bad mapping!!
|
|
if (triangleA.orientPreserving != triangleB.orientPreserving) {
|
|
bool chooseOrientFirstTri = false;
|
|
if (triangleB.groupWithAny)
|
|
chooseOrientFirstTri = true;
|
|
else if (calcTexArea(t) >= calcTexArea(t + 1))
|
|
chooseOrientFirstTri = true;
|
|
|
|
// force match
|
|
const uint t0 = chooseOrientFirstTri ? t : (t + 1);
|
|
const uint t1 = chooseOrientFirstTri ? (t + 1) : t;
|
|
triangles[t1].orientPreserving = triangles[t0].orientPreserving;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
|
/////////////////////////////////////////// Edges ///////////////////////////////////////////
|
|
|
|
struct NeighborShard {
|
|
struct Entry {
|
|
Entry(uint32_t key_, uint data_) : key(key_), data(data_) {}
|
|
uint key, data;
|
|
};
|
|
std::vector<Entry> entries;
|
|
|
|
NeighborShard(size_t capacity)
|
|
{
|
|
entries.reserve(capacity);
|
|
}
|
|
|
|
void buildNeighbors(Mikktspace<Mesh> *mikk)
|
|
{
|
|
/* Entries are added by iterating over t, so by using a stable sort,
|
|
* we don't have to compare based on t as well. */
|
|
{
|
|
std::vector<Entry> tempEntries(entries.size(), {0, 0});
|
|
radixsort(entries, tempEntries, [](const Entry &e) { return e.key; });
|
|
}
|
|
|
|
for (uint i = 0; i < entries.size(); i++) {
|
|
const Entry &a = entries[i];
|
|
uint tA, iA;
|
|
unpack_index(tA, iA, a.data);
|
|
Mikktspace<Mesh>::Triangle &triA = mikk->triangles[tA];
|
|
|
|
if (triA.neighbor[iA] != UNSET_ENTRY) {
|
|
continue;
|
|
}
|
|
|
|
uint i0A = triA.vertices[iA], i1A = triA.vertices[(iA != 2) ? (iA + 1) : 0];
|
|
for (uint j = i + 1; j < entries.size(); j++) {
|
|
const Entry &b = entries[j];
|
|
uint tB, iB;
|
|
unpack_index(tB, iB, b.data);
|
|
Mikktspace<Mesh>::Triangle &triB = mikk->triangles[tB];
|
|
|
|
if (b.key != a.key)
|
|
break;
|
|
|
|
if (triB.neighbor[iB] != UNSET_ENTRY) {
|
|
continue;
|
|
}
|
|
|
|
uint i1B = triB.vertices[iB], i0B = triB.vertices[(iB != 2) ? (iB + 1) : 0];
|
|
if (i0A == i0B && i1A == i1B) {
|
|
triA.neighbor[iA] = tB;
|
|
triB.neighbor[iB] = tA;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
void buildNeighbors()
|
|
{
|
|
/* In order to parallelize the processing, we divide the vertices into shards.
|
|
* Since only vertex pairs with the same key will be checked, we can process
|
|
* shards independently as long as we ensure that all vertices with the same
|
|
* key go into the same shard.
|
|
* This is done by hashing the key to get the shard index of each vertex.
|
|
*/
|
|
// TODO: Two-step filling that first counts and then fills? Could be parallel then.
|
|
uint targetNrShards = isParallel ? uint(4 * nrThreads) : 1;
|
|
uint nrShards = 1, hashShift = 32;
|
|
while (nrShards < targetNrShards) {
|
|
nrShards *= 2;
|
|
hashShift -= 1;
|
|
}
|
|
|
|
/* Reserve 25% extra to account for variation due to hashing. */
|
|
size_t reserveSize = size_t(double(3 * nrTriangles) * 1.25 / nrShards);
|
|
std::vector<NeighborShard> shards(nrShards, {reserveSize});
|
|
|
|
for (uint t = 0; t < nrTriangles; t++) {
|
|
Triangle &triangle = triangles[t];
|
|
for (uint i = 0; i < 3; i++) {
|
|
const uint i0 = triangle.vertices[i];
|
|
const uint i1 = triangle.vertices[(i != 2) ? (i + 1) : 0];
|
|
const uint high = std::max(i0, i1), low = std::min(i0, i1);
|
|
const uint hash = hash_uint3(high, low, 0);
|
|
/* TODO: Reusing the hash here means less hash space inside each shard.
|
|
* Computing a second hash with a different seed it probably not worth it? */
|
|
const uint shard = isParallel ? (hash >> hashShift) : 0;
|
|
shards[shard].entries.emplace_back(hash, pack_index(t, i));
|
|
}
|
|
}
|
|
|
|
runParallel(0u, nrShards, [&](uint s) { shards[s].buildNeighbors(this); });
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
void assignRecur(const uint t, uint groupId)
|
|
{
|
|
if (t == UNSET_ENTRY) {
|
|
return;
|
|
}
|
|
|
|
Triangle &triangle = triangles[t];
|
|
Group &group = groups[groupId];
|
|
|
|
// track down vertex
|
|
const uint vertRep = group.vertexRepresentative;
|
|
uint i = 3;
|
|
if (triangle.vertices[0] == vertRep)
|
|
i = 0;
|
|
else if (triangle.vertices[1] == vertRep)
|
|
i = 1;
|
|
else if (triangle.vertices[2] == vertRep)
|
|
i = 2;
|
|
assert(i < 3);
|
|
|
|
// early out
|
|
if (triangle.group[i] != UNSET_ENTRY)
|
|
return;
|
|
|
|
if (triangle.groupWithAny) {
|
|
// first to group with a group-with-anything triangle
|
|
// determines its orientation.
|
|
// This is the only existing order dependency in the code!!
|
|
if (triangle.group[0] == UNSET_ENTRY && triangle.group[1] == UNSET_ENTRY &&
|
|
triangle.group[2] == UNSET_ENTRY) {
|
|
triangle.orientPreserving = group.orientPreserving;
|
|
}
|
|
}
|
|
|
|
if (triangle.orientPreserving != group.orientPreserving)
|
|
return;
|
|
|
|
triangle.group[i] = groupId;
|
|
|
|
const uint t_L = triangle.neighbor[i];
|
|
const uint t_R = triangle.neighbor[i > 0 ? (i - 1) : 2];
|
|
assignRecur(t_L, groupId);
|
|
assignRecur(t_R, groupId);
|
|
}
|
|
|
|
void build4RuleGroups()
|
|
{
|
|
/* NOTE: This could be parallelized by grouping all [t, i] pairs into
|
|
* shards by hash(triangles[t].vertices[i]). This way, each shard can be processed
|
|
* independently and in parallel.
|
|
* However, the `groupWithAny` logic needs special handling (e.g. lock a mutex when
|
|
* encountering a `groupWithAny` triangle, then sort it out, then unlock and proceed). */
|
|
for (uint t = 0; t < nrTriangles; t++) {
|
|
Triangle &triangle = triangles[t];
|
|
for (uint i = 0; i < 3; i++) {
|
|
// if not assigned to a group
|
|
if (triangle.groupWithAny || triangle.group[i] != UNSET_ENTRY) {
|
|
continue;
|
|
}
|
|
|
|
const uint newGroupId = uint(groups.size());
|
|
triangle.group[i] = newGroupId;
|
|
|
|
groups.emplace_back(triangle.vertices[i], bool(triangle.orientPreserving));
|
|
|
|
const uint t_L = triangle.neighbor[i];
|
|
const uint t_R = triangle.neighbor[i > 0 ? (i - 1) : 2];
|
|
assignRecur(t_L, newGroupId);
|
|
assignRecur(t_R, newGroupId);
|
|
}
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
template<bool atomic> void accumulateTSpaces(uint t)
|
|
{
|
|
const Triangle &triangle = triangles[t];
|
|
// only valid triangles get to add their contribution
|
|
if (triangle.groupWithAny) {
|
|
return;
|
|
}
|
|
|
|
/* Todo: Vectorize?
|
|
* Also: Could add special case for flat shading, when all normals are equal half of the fCos
|
|
* projections and two of the three tangent projections are unnecessary. */
|
|
std::array<float3, 3> n, p;
|
|
for (uint i = 0; i < 3; i++) {
|
|
n[i] = getNormal(triangle.vertices[i]);
|
|
p[i] = getPosition(triangle.vertices[i]);
|
|
}
|
|
|
|
std::array<float, 3> fCos = {dot(project(n[0], p[1] - p[0]), project(n[0], p[2] - p[0])),
|
|
dot(project(n[1], p[2] - p[1]), project(n[1], p[0] - p[1])),
|
|
dot(project(n[2], p[0] - p[2]), project(n[2], p[1] - p[2]))};
|
|
|
|
for (uint i = 0; i < 3; i++) {
|
|
uint groupId = triangle.group[i];
|
|
if (groupId != UNSET_ENTRY) {
|
|
float3 tangent = project(n[i], triangle.tangent) *
|
|
fast_acosf(std::clamp(fCos[i], -1.0f, 1.0f));
|
|
if constexpr (atomic) {
|
|
groups[groupId].accumulateTSpaceAtomic(tangent);
|
|
}
|
|
else {
|
|
groups[groupId].accumulateTSpace(tangent);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void generateTSpaces()
|
|
{
|
|
if (isParallel) {
|
|
runParallel(0u, nrTriangles, [&](uint t) { accumulateTSpaces<true>(t); });
|
|
}
|
|
else {
|
|
for (uint t = 0; t < nrTriangles; t++) {
|
|
accumulateTSpaces<false>(t);
|
|
}
|
|
}
|
|
|
|
/* TODO: Worth parallelizing? Probably not. */
|
|
for (Group &group : groups) {
|
|
group.normalizeTSpace();
|
|
}
|
|
|
|
tSpaces.resize(nrTSpaces);
|
|
|
|
for (uint t = 0; t < nrTriangles; t++) {
|
|
Triangle &triangle = triangles[t];
|
|
for (uint i = 0; i < 3; i++) {
|
|
uint groupId = triangle.group[i];
|
|
if (groupId == UNSET_ENTRY) {
|
|
continue;
|
|
}
|
|
const Group group = groups[groupId];
|
|
assert(triangle.orientPreserving == group.orientPreserving);
|
|
|
|
// output tspace
|
|
const uint offset = triangle.tSpaceIdx;
|
|
const uint faceVertex = triangle.faceVertex[i];
|
|
tSpaces[offset + faceVertex].accumulateGroup(group);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
} // namespace mikk
|