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