Skip to content

Commit

Permalink
Added a check to remove degenerate/microscopic triangles from Transvo…
Browse files Browse the repository at this point in the history
…xel.

This is documented in the Transvoxel paper. We ignored it so far because
it is quite rare (less than 1% of meshes in typical terrain) and did not
cause problems in practice... so far.

This is to workaround errors thrown by Jolt Physics, which doesn't ignore
them instead. Microscopic triangles can also cause meshes to become empty
and cause further errors, due to Jolt having to re-index meshes passed
from the Godot Physics3DServer API.

Unfortunately, that change makes meshing 15% slower (texturing off).
  • Loading branch information
Zylann committed Dec 29, 2023
1 parent 07f99ff commit e5d8fd4
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 2 deletions.
1 change: 1 addition & 0 deletions doc/source/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ Semver is not yet in place, so each version can have breaking changes, although
- `VoxelTerrain`: Fixed crash when the terrain tries to update while it has no mesher assigned
- `VoxelLodTerrain`: Fixed error spam when re-generating or destroying the terrain
- `VoxelMesherBlocky`: Fixed materials "wrapping around" when more than 256 are used. Raised limit to 65536.
- `VoxelMesherTransvoxel`: Removed rare degenerate/microscopic triangles, which caused errors with Jolt Physics. However, doing those checks makes meshing about 15% slower (untextured).
- `VoxelStreamRegionFiles`: Fixed `block_size_po2` wasn't working correctly
- `VoxelToolTerrain`: Fixed terrain was not marked as modified when setting voxel metadata
- `VoxelToolLodTerrain`:
Expand Down
55 changes: 53 additions & 2 deletions meshers/transvoxel/transvoxel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include "../../constants/cube_tables.h"
#include "../../util/godot/core/sort_array.h"
#include "../../util/math/conv.h"
#include "../../util/math/triangle.h"
#include "../../util/profiling.h"
#include "transvoxel_tables.cpp"

Expand Down Expand Up @@ -677,18 +678,62 @@ void build_regular_mesh(Span<const Sdf_T> sdf_data, TextureIndicesData texture_i

} // for each cell vertex

uint32_t effective_triangle_count = triangle_count;

for (int t = 0; t < triangle_count; ++t) {
const int t0 = t * 3;

const int i0 = cell_vertex_indices[regular_cell_data.get_vertex_index(t0)];
const int i1 = cell_vertex_indices[regular_cell_data.get_vertex_index(t0 + 1)];
const int i2 = cell_vertex_indices[regular_cell_data.get_vertex_index(t0 + 2)];

{
// Transvoxel paper:
// It is possible to generate triangles having zero area when one or more of the corner sample
// values for a cell is zero. For example, when we triangulate a cell for which one corner
// sample value is zero and the seven remaining corner sample values are negative, then we
// generate the single triangle of equivalence class #1 (see Table 3.2). However, all three
// vertices lie exactly at the corner having zero sample value. Such triangles are eliminated
// after a simple area calculation indicates that they are degenerate.
//
// Not fixing this used to work fine actually, but Jolt physics integration makes this
// problematic. Jolt checks for degenerate triangles, but instead of just skipping them, it
// throws errors. Also, the fact Godot enforces passing a de-indexed mesh through the
// Physics3DServer requires Jolt to re-index it. This is not only a waste of time, but also,
// Jolt eliminates vertices at the same location or below a hardcoded threshold in the process.
// This in turn causes further issues, as degenerate or microscopic triangles cause the
// same errors, instead of just being ignored. So a workaround is to actively remove those
// triangles here, at the cost of extra CPU work.
//
// Note, this workaround means there can be unused vertices in the final mesh.
// Another workaround could have been to alter the SDF to never have 0, but that would not
// cover the case of triangles that are too thin.
//
// Profiling results, in average time per chunk (16^3).
// With it: 75 us
// Without it: 65 us
// So fixing the 0.5% of meshes with at least 1 degenerate/superthin triangle isn't negligible
// unfortunately.
//
// About Jolt re-indexing meshes, see PR (abandoned?):
// https://github.com/godotengine/godot/pull/72868
//
const Vector3f p0 = output.vertices[i0];
const Vector3f p1 = output.vertices[i1];
const Vector3f p2 = output.vertices[i2];
if (math::is_triangle_degenerate_approx(p0, p1, p2, 0.000001f)) {
--effective_triangle_count;
continue;
}
}

output.indices.push_back(i0);
output.indices.push_back(i1);
output.indices.push_back(i2);
}

if (cell_info != nullptr) {
cell_info->push_back(CellInfo{ pos - min_pos, triangle_count });
cell_info->push_back(CellInfo{ pos - min_pos, effective_triangle_count });
}

} // x
Expand Down Expand Up @@ -1302,7 +1347,13 @@ std::vector<uint16_t> &get_tls_weights_backing_buffer_u16() {
// It must be done on the whole buffer to ensure consistency (and not after early cell rejection),
// otherwise it can create gaps in the final mesh.
//
// Not used anymore for now, but if we get this problem again we may have to investigate
// Not used anymore for now, but if we get this problem again we may have to investigate.
//
// 2023/12/28 update:
// Jolt physics really doesn't like degenerate meshes and throws errors, unlike GodotPhysics and Bullet. Entire meshes
// can be like that because there could be a voxel buffer filled with > 0 SDF, except one which is zero, and since zero
// is considered "inside", case 223 triggers and produces a bunch of vertices at the exact same position.
// Fixed for now by eliminating triangles.
//
/*template <typename Sdf_T>
Span<const Sdf_T> apply_zero_sdf_fix(Span<const Sdf_T> p_sdf_data) {
Expand Down
4 changes: 4 additions & 0 deletions meshers/transvoxel/voxel_mesher_transvoxel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,10 @@ void VoxelMesherTransvoxel::build(VoxelMesher::Output &output, const VoxelMesher
// The mesh can be empty
return;
}
if (mesh_arrays.indices.size() == 0) {
// The mesh can have vertices, but still be empty, for example because triangles are all degenerate
return;
}

transvoxel::MeshArrays *combined_mesh_arrays = &mesh_arrays;
if (_mesh_optimization_params.enabled) {
Expand Down
15 changes: 15 additions & 0 deletions util/math/triangle.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,21 @@ inline bool is_point_in_triangle(const Vector2f &s, const Vector2f &a, const Vec
return (cross(cn, an) > 0) == orientation;
}

inline float get_triangle_area_squared_x2(Vector3f p0, Vector3f p1, Vector3f p2) {
const Vector3f p01 = p1 - p0;
const Vector3f p02 = p2 - p0;
const Vector3f c = math::cross(p01, p02);
return math::length_squared(c);
}

inline float is_triangle_degenerate_approx(Vector3f p0, Vector3f p1, Vector3f p2, float epsilon_squared) {
return get_triangle_area_squared_x2(p0, p1, p2) < epsilon_squared;
}

// inline float is_triangle_degenerate(Vector3f p0, Vector3f p1, Vector3f p2) {
// return p0 == p1 || p1 == p2 || p2 == p0;
// }

// Heron's formula is overly represented on SO but uses 4 square roots. This uses only one.
// A parallelogram's area is found with the magnitude of the cross product of two adjacent side vectors,
// so a triangle's area is half of it
Expand Down

0 comments on commit e5d8fd4

Please sign in to comment.