Skip to content

Commit

Permalink
#5576: Implement a similar hash bucket behaviour as used in the engin…
Browse files Browse the repository at this point in the history
…e code, using the same epsilons as defined in the default values of the CVARs.
  • Loading branch information
codereader committed Apr 5, 2021
1 parent 678bdd9 commit d175b0f
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 66 deletions.
7 changes: 7 additions & 0 deletions libs/render/TexCoord2f.h
Expand Up @@ -49,4 +49,11 @@ class TexCoord2f :
}
return false;
}

template<typename Epsilon>
bool isEqual(const TexCoord2f& other, Epsilon epsilon) const
{
return float_equal_epsilon(x(), other.x(), epsilon) &&
float_equal_epsilon(y(), other.y(), epsilon);
}
};
69 changes: 3 additions & 66 deletions radiantcore/model/import/AseModel.cpp
Expand Up @@ -6,6 +6,8 @@
#include "string/case_conv.h"
#include "string/trim.h"

#include "Hashing.h"

/* -----------------------------------------------------------------------------
ASE Loading Code based on the original PicoModel ASE parser (licence as follows)
Expand Down Expand Up @@ -42,59 +44,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
----------------------------------------------------------------------------- */

namespace
{
inline void combineHash(std::size_t& seed, std::size_t hash)
{
seed ^= hash + 0x9e3779b9 + (seed << 6) + (seed >> 2);
}
}

template<>
struct std::hash<Vector3>
{
size_t operator()(const Vector3& v) const
{
std::hash<double> hasher;

auto hash = hasher(v.x());
combineHash(hash, hasher(v.y()));
combineHash(hash, hasher(v.z()));

return hash;
}
};

// Hash specialisation such that ArbitraryMeshVertex can be used as key type in std::unordered_map<>
template<>
struct std::hash<ArbitraryMeshVertex>
{
size_t operator()(const ArbitraryMeshVertex& v) const
{
std::hash<Vector3> vectorHash;
std::hash<double> doubleHash;

auto hash = vectorHash(v.vertex);

combineHash(hash, vectorHash(v.normal));
combineHash(hash, doubleHash(v.texcoord.x()));
combineHash(hash, doubleHash(v.texcoord.y()));
combineHash(hash, vectorHash(v.colour));

return hash;
}
};

// Assumes equality of two ArbitraryMeshVertices if all of (vertex, normal, texcoord, colour) are equal
template<>
struct std::equal_to<ArbitraryMeshVertex>
{
bool operator()(const ArbitraryMeshVertex& a, const ArbitraryMeshVertex& b) const
{
return a.vertex == b.vertex && a.normal == b.normal && a.texcoord == b.texcoord && a.colour == b.colour;
}
};

namespace model
{

Expand Down Expand Up @@ -142,13 +91,11 @@ void AseModel::finishSurface(Mesh& mesh, std::size_t materialIndex, const Matrix
double materialSin = sin(material.uvAngle);
double materialCos = cos(material.uvAngle);

//
// Hash table to provide quick (and coarse) lookup of vertices with similar XYZ coords
std::unordered_map<ArbitraryMeshVertex, std::size_t> vertexIndices;

for (const auto& face : mesh.faces)
{
//rMessage() << "We got " << mesh.faces.size() << " faces, that's " << mesh.faces.size() * 3 << " vertices to inspect" << std::endl;

// we pull the data from the vertex, color and texcoord arrays using the face index data
for (int j = 0; j < 3; ++j)
{
Expand Down Expand Up @@ -179,21 +126,11 @@ void AseModel::finishSurface(Mesh& mesh, std::size_t materialIndex, const Matrix
colour
);

//rMessage() << "---" << std::endl;
//rMessage() << "New vertex: " << meshVertex << std::endl;
//rMessage() << "Existing vertices: " << std::endl;

//for (const auto& pair : vertexIndices)
//{
// rMessage() << pair.second << " : " << pair.first << std::endl;
//}

// Try to look up an existing vertex or add a new index
auto emplaceResult = vertexIndices.try_emplace(meshVertex, surface.vertices.size());

if (emplaceResult.second)
{
//rMessage() << "This was a new one" << std::endl;
// This was a new vertex, copy it to the vertex array
surface.vertices.emplace_back(emplaceResult.first->first);
}
Expand Down
93 changes: 93 additions & 0 deletions radiantcore/model/import/Hashing.h
@@ -0,0 +1,93 @@
#pragma once

#include "math/Vector3.h"
#include "render/ArbitraryMeshVertex.h"

/**
* greebo: When creating model surfaces from ASE models, the in-game loading code
* will aim to merge vertices that are near each other (and have otherwise negligible
* differences for their other components like normal, texcoords and colour).
*
* The in-game code is using a custom vertex hash index implementation to quickly
* look up almost-similar vertices and prevent such "duplicates" from ending up in the
* final mesh. This is based on a set of epsilon (one for each type, in CVARs like
* r_slopVertex, r_slopNormal, etc.) and to speed things up, a hash index is built
* for all the vertices in a single mesh.
*
* The hash functors below aim to reproduce this behaviour without fully re-implementing
* those custom containers in the engine code, designed to be used in a std::unordered_map
* with ArbitraryMeshVertex used as Key type.
*
* The ArbitraryMeshVertex hash functions is deliberately coarse and will produce lots of
* collisions for vertices in a certain vicinity, only provide a fast way of looking up
* duplicates in meshes with many verts. Each "colliding" vertex will then be compared
* in-depth by the std::equal_to<> specialisation, where the epsilon-comparison is performed.
*/
namespace
{
// A hash combination function based on the one used in boost and found on stackoverflow
inline void combineHash(std::size_t& seed, std::size_t hash)
{
seed ^= hash + 0x9e3779b9 + (seed << 6) + (seed >> 2);
}

// These epsilons below correspond to the CVARs in the game code
constexpr double VertexEpsilon = 0.01; // r_slopVertex
constexpr double NormalEpsilon = 0.02; // r_slopNormal
constexpr double TexCoordEpsilon = 0.001; // r_slopTexCoord

// Delivers 10.0^signficantDigits
constexpr double RoundingFactor(std::size_t significantVertexDigits)
{
return significantVertexDigits == 1 ? 10.0 : 10.0 * RoundingFactor(significantVertexDigits - 1);
}
}

// Coarse hash of the 3-component vector
// This is rounding the doubles to the SignificantVertexDigits defined above,
// so any two vectors with their components only differing after the few significant digits
// will produce the same hash.
template<>
struct std::hash<Vector3>
{
static constexpr std::size_t SignificantVertexDigits = 2;

size_t operator()(const Vector3& v) const
{
auto xHash = static_cast<std::size_t>(v.x() * RoundingFactor(SignificantVertexDigits));
auto yHash = static_cast<std::size_t>(v.y() * RoundingFactor(SignificantVertexDigits));
auto zHash = static_cast<std::size_t>(v.z() * RoundingFactor(SignificantVertexDigits));

combineHash(xHash, yHash);
combineHash(xHash, zHash);

return xHash;
}
};

// Hash specialisation such that ArbitraryMeshVertex can be used as key type in std::unordered_map<>
// Only the 3D coordinates (the vertex member) will be considered for hash calculation,
// to intentionally produce hash collisions for vectors that are within a certain VertexEpsilon.
template<>
struct std::hash<ArbitraryMeshVertex>
{
size_t operator()(const ArbitraryMeshVertex& v) const
{
// We just hash the vertex
return std::hash<Vector3>()(v.vertex);
}
};

// Assumes equality of two ArbitraryMeshVertices if all of (vertex, normal, texcoord, colour)
// are equal within defined epsilons (VertexEpsilon, NormalEpsilon, TexCoordEpsilon)
template<>
struct std::equal_to<ArbitraryMeshVertex>
{
bool operator()(const ArbitraryMeshVertex& a, const ArbitraryMeshVertex& b) const
{
return a.vertex.isEqual(b.vertex, VertexEpsilon) &&
a.normal.dot(b.normal) > (1.0 - NormalEpsilon) &&
a.texcoord.isEqual(b.texcoord, TexCoordEpsilon) &&
a.colour.isEqual(b.colour, VertexEpsilon);
}
};
1 change: 1 addition & 0 deletions tools/msvc/DarkRadiantCore.vcxproj
Expand Up @@ -860,6 +860,7 @@
<ClInclude Include="..\..\radiantcore\model\export\WavefrontExporter.h" />
<ClInclude Include="..\..\radiantcore\model\import\AseModel.h" />
<ClInclude Include="..\..\radiantcore\model\import\AseModelLoader.h" />
<ClInclude Include="..\..\radiantcore\model\import\Hashing.h" />
<ClInclude Include="..\..\radiantcore\model\import\ModelImporterBase.h" />
<ClInclude Include="..\..\radiantcore\model\md5\MD5Anim.h" />
<ClInclude Include="..\..\radiantcore\model\md5\MD5AnimationCache.h" />
Expand Down
3 changes: 3 additions & 0 deletions tools/msvc/DarkRadiantCore.vcxproj.filters
Expand Up @@ -2115,5 +2115,8 @@
<ClInclude Include="..\..\radiantcore\model\import\AseModel.h">
<Filter>src\model\import</Filter>
</ClInclude>
<ClInclude Include="..\..\radiantcore\model\import\Hashing.h">
<Filter>src\model\import</Filter>
</ClInclude>
</ItemGroup>
</Project>

0 comments on commit d175b0f

Please sign in to comment.