From e5d8fd4dad28dc27bf47d5eac4a4283c08100be1 Mon Sep 17 00:00:00 2001 From: Marc Gilleron Date: Fri, 29 Dec 2023 18:20:28 +0000 Subject: [PATCH] Added a check to remove degenerate/microscopic triangles from Transvoxel. 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). --- doc/source/changelog.md | 1 + meshers/transvoxel/transvoxel.cpp | 55 ++++++++++++++++++- .../transvoxel/voxel_mesher_transvoxel.cpp | 4 ++ util/math/triangle.h | 15 +++++ 4 files changed, 73 insertions(+), 2 deletions(-) diff --git a/doc/source/changelog.md b/doc/source/changelog.md index cb61c0d97..4f79db3f9 100644 --- a/doc/source/changelog.md +++ b/doc/source/changelog.md @@ -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`: diff --git a/meshers/transvoxel/transvoxel.cpp b/meshers/transvoxel/transvoxel.cpp index 4fd5b9fc2..12ac90d25 100644 --- a/meshers/transvoxel/transvoxel.cpp +++ b/meshers/transvoxel/transvoxel.cpp @@ -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" @@ -677,18 +678,62 @@ void build_regular_mesh(Span 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 @@ -1302,7 +1347,13 @@ std::vector &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 Span apply_zero_sdf_fix(Span p_sdf_data) { diff --git a/meshers/transvoxel/voxel_mesher_transvoxel.cpp b/meshers/transvoxel/voxel_mesher_transvoxel.cpp index 5df25e628..cd951af33 100644 --- a/meshers/transvoxel/voxel_mesher_transvoxel.cpp +++ b/meshers/transvoxel/voxel_mesher_transvoxel.cpp @@ -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) { diff --git a/util/math/triangle.h b/util/math/triangle.h index 2753061a4..d47e34370 100644 --- a/util/math/triangle.h +++ b/util/math/triangle.h @@ -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