From 7aa33267a8874385728f38b442615400a7ce3877 Mon Sep 17 00:00:00 2001 From: Marc Gilleron Date: Sat, 8 Feb 2020 23:05:44 +0000 Subject: [PATCH] Culling masks improvements: - Use a 64-bit mask instead of 8-bit - Fix face separation when a custom mesh is used - Automatically generate culling masks using raster approximation --- meshers/blocky/voxel.cpp | 201 ++++++++++++++++++------- meshers/blocky/voxel.h | 13 +- meshers/blocky/voxel_library.cpp | 10 +- meshers/blocky/voxel_library.h | 3 +- meshers/blocky/voxel_mesher_blocky.cpp | 4 +- 5 files changed, 165 insertions(+), 66 deletions(-) diff --git a/meshers/blocky/voxel.cpp b/meshers/blocky/voxel.cpp index 5ce7e670..4fa25f51 100644 --- a/meshers/blocky/voxel.cpp +++ b/meshers/blocky/voxel.cpp @@ -218,31 +218,32 @@ void Voxel::set_custom_mesh(Ref mesh) { ERR_FAIL_COND(normals.size() == 0); struct L { - static bool get_side(Vector3 pos, Cube::SideAxis &out_side) { - if (Math::is_equal_approx(pos.x, 0.0)) { - out_side = Cube::SIDE_NEGATIVE_X; - return true; + static uint8_t get_sides(Vector3 pos) { + uint8_t mask = 0; + const real_t tolerance = 0.001; + mask |= Math::is_equal_approx(pos.x, 0.0, tolerance) << Cube::SIDE_NEGATIVE_X; + mask |= Math::is_equal_approx(pos.x, 1.0, tolerance) << Cube::SIDE_POSITIVE_X; + mask |= Math::is_equal_approx(pos.y, 0.0, tolerance) << Cube::SIDE_NEGATIVE_Y; + mask |= Math::is_equal_approx(pos.y, 1.0, tolerance) << Cube::SIDE_POSITIVE_Y; + mask |= Math::is_equal_approx(pos.z, 0.0, tolerance) << Cube::SIDE_NEGATIVE_Z; + mask |= Math::is_equal_approx(pos.z, 1.0, tolerance) << Cube::SIDE_POSITIVE_Z; + return mask; + } + + static bool get_triangle_side(const Vector3 &a, const Vector3 &b, const Vector3 &c, Cube::SideAxis &out_side) { + const uint8_t m = get_sides(a) & get_sides(b) & get_sides(c); + if (m == 0) { + // At least one of the points doesn't belong to a face + return false; } - if (Math::is_equal_approx(pos.x, 1.0)) { - out_side = Cube::SIDE_POSITIVE_X; - return true; - } - if (Math::is_equal_approx(pos.y, 0.0)) { - out_side = Cube::SIDE_NEGATIVE_Y; - return true; - } - if (Math::is_equal_approx(pos.y, 1.0)) { - out_side = Cube::SIDE_POSITIVE_Y; - return true; - } - if (Math::is_equal_approx(pos.z, 0.0)) { - out_side = Cube::SIDE_NEGATIVE_Z; - return true; - } - if (Math::is_equal_approx(pos.z, 1.0)) { - out_side = Cube::SIDE_POSITIVE_Z; - return true; + for (unsigned int side = 0; side < Cube::SIDE_COUNT; ++side) { + if (m == (1 << side)) { + // All points belong to the same face + out_side = (Cube::SideAxis)side; + return true; + } } + // The triangle isn't in one face return false; } }; @@ -265,40 +266,39 @@ void Voxel::set_custom_mesh(Ref mesh) { FixedArray, Cube::SIDE_COUNT> added_side_indices; HashMap added_regular_indices; + FixedArray tri_positions; for (int i = 0; i < indices.size(); i += 3) { - Cube::SideAxis side0; - Cube::SideAxis side1; - Cube::SideAxis side2; + Cube::SideAxis side; - if (L::get_side(positions_read[indices_read[i]], side0) && - L::get_side(positions_read[indices_read[i + 1]], side1) && - L::get_side(positions_read[indices_read[i + 2]], side2) && - side0 == side1 && - side1 == side2) { + tri_positions[0] = positions_read[indices_read[i]]; + tri_positions[1] = positions_read[indices_read[i + 1]]; + tri_positions[2] = positions_read[indices_read[i + 2]]; + + if (L::get_triangle_side(tri_positions[0], tri_positions[1], tri_positions[2], side)) { // That triangle is on the face - int next_side_index = _model_side_positions[side0].size(); + int next_side_index = _model_side_positions[side].size(); for (int j = 0; j < 3; ++j) { int src_index = indices_read[i + j]; - const int *existing_dst_index = added_side_indices[side0].getptr(src_index); + const int *existing_dst_index = added_side_indices[side].getptr(src_index); if (existing_dst_index == nullptr) { // Add new vertex - _model_side_indices[side0].push_back(next_side_index); - _model_side_positions[side0].push_back(positions_read[indices_read[i + j]]); - _model_side_uvs[side0].push_back(uvs_read[indices_read[i + j]]); + _model_side_indices[side].push_back(next_side_index); + _model_side_positions[side].push_back(tri_positions[j]); + _model_side_uvs[side].push_back(uvs_read[indices_read[i + j]]); - added_side_indices[side0].set(src_index, next_side_index); + added_side_indices[side].set(src_index, next_side_index); ++next_side_index; } else { // Vertex was already added, just add index referencing it - _model_side_indices[side0].push_back(*existing_dst_index); + _model_side_indices[side].push_back(*existing_dst_index); } } @@ -314,7 +314,7 @@ void Voxel::set_custom_mesh(Ref mesh) { if (existing_dst_index == nullptr) { _model_indices.push_back(next_regular_index); - _model_positions.push_back(positions_read[indices_read[i + j]]); + _model_positions.push_back(tri_positions[j]); _model_normals.push_back(normals_read[indices_read[i + j]]); _model_uvs.push_back(uvs_read[indices_read[i + j]]); @@ -329,10 +329,7 @@ void Voxel::set_custom_mesh(Ref mesh) { } } - // TODO Expose side masks in the inspector, somehow - for (unsigned int side = 0; side < Cube::SIDE_COUNT; ++side) { - _side_culling_masks[side] = 0; - } + generate_side_culling_masks(); } Ref Voxel::set_cube_geometry(float sy) { @@ -356,15 +353,13 @@ Ref Voxel::set_cube_geometry(float sy) { for (unsigned int i = 0; i < 6; ++i) { indices[i] = Cube::g_side_quad_triangles[side][i]; } - - if (side != Cube::SIDE_POSITIVE_Y || sy == 1.0) { - _side_culling_masks[side] = 0xff; - } } _collision_aabbs.clear(); _collision_aabbs.push_back(AABB(Vector3(0, 0, 0), Vector3(1, 1, 1))); + generate_side_culling_masks(); + return Ref(this); } @@ -407,6 +402,105 @@ void Voxel::update_cube_uv_sides() { } } +void Voxel::set_side_culling_mask(int side, uint64_t mask) { + _side_culling_masks[side] = mask; +} + +template +static void rasterize_triangle_barycentric(Vector2 a, Vector2 b, Vector2 c, F output_func) { + // Slower than scanline method, but looks better + + // Grow the triangle a tiny bit, to help against floating point error + Vector2 m = 0.333333 * (a + b + c); + a += 0.001 * (a - m); + b += 0.001 * (b - m); + c += 0.001 * (c - m); + + int min_x = (int)Math::floor(min(min(a.x, b.x), c.x)); + int min_y = (int)Math::floor(min(min(a.y, b.y), c.y)); + int max_x = (int)Math::ceil(max(max(a.x, b.x), c.x)); + int max_y = (int)Math::ceil(max(max(a.y, b.y), c.y)); + + // We test against points centered on grid cells + Vector2 offset(0.5, 0.5); + + for (int y = min_y; y < max_y; ++y) { + for (int x = min_x; x < max_x; ++x) { + if (Geometry::is_point_in_triangle(Vector2(x, y) + offset, a, b, c)) { + output_func(x, y); + } + } + } +} + +void Voxel::generate_side_culling_masks() { + + // When two blocky voxels are next to each other, they share a side. + // Geometry of either side can be culled away if covered by the other, + // but it's very expensive to do a full polygon check when we build the mesh. + // So instead, we compute which sides occlude which for every voxel type, + // and generate culling masks ahead of time, using an approximation. + // It may have a limitation of the number of different side types, + // so it's a tradeoff to take when designing the models. + + for (uint16_t side = 0; side < Cube::SIDE_COUNT; ++side) { + const std::vector &positions = get_model_side_positions(side); + const std::vector &indices = get_model_side_indices(side); + ERR_FAIL_COND(indices.size() % 3 != 0); + + uint64_t raster_mask = 0; + + for (unsigned int j = 0; j < indices.size(); j += 3) { + + Vector3 va = positions[indices[j]]; + Vector3 vb = positions[indices[j + 1]]; + Vector3 vc = positions[indices[j + 2]]; + + // Convert 3D vertices into 2D + Vector2 a, b, c; + switch (side) { + case Cube::SIDE_NEGATIVE_X: + case Cube::SIDE_POSITIVE_X: + a = Vector2(va.y, va.z); + b = Vector2(vb.y, vb.z); + c = Vector2(vc.y, vc.z); + break; + + case Cube::SIDE_NEGATIVE_Y: + case Cube::SIDE_POSITIVE_Y: + a = Vector2(va.x, va.z); + b = Vector2(vb.x, vb.z); + c = Vector2(vc.x, vc.z); + break; + + case Cube::SIDE_NEGATIVE_Z: + case Cube::SIDE_POSITIVE_Z: + a = Vector2(va.x, va.y); + b = Vector2(vb.x, vb.y); + c = Vector2(vc.x, vc.y); + break; + + default: + CRASH_NOW(); + } + + a *= 8; + b *= 8; + c *= 8; + + // Rasterize triangles into an 8x8 grid + rasterize_triangle_barycentric(a, b, c, [&raster_mask](uint64_t x, uint64_t y) { + if (x >= 8 || y >= 8) { + return; + } + raster_mask |= (uint64_t(1) << (x + uint64_t(8) * y)); + }); + } + + set_side_culling_mask(side, raster_mask); + } +} + //Ref Voxel::set_xquad_geometry(Vector2 atlas_pos) { // // TODO // return Ref(this); @@ -438,8 +532,9 @@ void Voxel::_bind_methods() { ClassDB::bind_method(D_METHOD("set_collision_aabbs", "aabbs"), &Voxel::_b_set_collision_aabbs); ClassDB::bind_method(D_METHOD("get_collision_aabbs"), &Voxel::_b_get_collision_aabbs); - ClassDB::bind_method(D_METHOD("set_face_culling_mask", "side", "mask"), &Voxel::_b_set_face_culling_mask); - ClassDB::bind_method(D_METHOD("get_face_culling_mask"), &Voxel::_b_get_face_culling_mask); + ClassDB::bind_method(D_METHOD("set_side_culling_mask", "side", "mask"), &Voxel::_b_set_side_culling_mask); + ClassDB::bind_method(D_METHOD("get_side_culling_mask"), &Voxel::_b_get_side_culling_mask); + //ClassDB::bind_method(D_METHOD("generate_side_culling_masks"), &Voxel::generate_side_culling_masks); ADD_PROPERTY(PropertyInfo(Variant::STRING, "voxel_name"), "set_voxel_name", "get_voxel_name"); ADD_PROPERTY(PropertyInfo(Variant::COLOR, "color"), "set_color", "get_color"); @@ -484,12 +579,12 @@ void Voxel::_b_set_collision_aabbs(Array array) { } } -void Voxel::_b_set_face_culling_mask(Side face_id, uint8_t mask) { +void Voxel::_b_set_side_culling_mask(Side face_id, uint64_t mask) { ERR_FAIL_INDEX(face_id, SIDE_COUNT); - _side_culling_masks[face_id] = mask; + set_side_culling_mask(face_id, mask); } -uint8_t Voxel::_b_get_face_culling_mask(int face_id) const { +uint64_t Voxel::_b_get_side_culling_mask(int face_id) const { ERR_FAIL_INDEX_V(face_id, SIDE_COUNT, 0); - return _side_culling_masks[face_id]; + return get_side_culling_mask(face_id); } diff --git a/meshers/blocky/voxel.h b/meshers/blocky/voxel.h index 7f7d10ff..df82dab8 100644 --- a/meshers/blocky/voxel.h +++ b/meshers/blocky/voxel.h @@ -47,6 +47,11 @@ public: void set_custom_mesh(Ref mesh); Ref get_custom_mesh() const { return _custom_mesh; } + void set_side_culling_mask(int side, uint64_t mask); + inline uint64_t get_side_culling_mask(int side) const { return _side_culling_masks[side]; } + + void generate_side_culling_masks(); + //------------------------------------------- // Built-in geometry generators @@ -75,8 +80,6 @@ public: void set_library(Ref lib); - inline uint8_t get_face_culling_mask(int side) const { return _side_culling_masks[side]; } - private: bool _set(const StringName &p_name, const Variant &p_value); bool _get(const StringName &p_name, Variant &r_ret) const; @@ -96,8 +99,8 @@ private: Array _b_get_collision_aabbs() const; void _b_set_collision_aabbs(Array array); - void _b_set_face_culling_mask(Side face_id, uint8_t mask); - uint8_t _b_get_face_culling_mask(int face_id) const; + void _b_set_side_culling_mask(Side face_id, uint64_t mask); + uint64_t _b_get_side_culling_mask(int face_id) const; private: ObjectID _library; @@ -118,7 +121,7 @@ private: // If a face touches a neighbor face, this decides if it gets culled. // If the neighbor's face culling mask has all bits of the current face's mask, the face will be culled. - FixedArray _side_culling_masks; + FixedArray _side_culling_masks; // Model std::vector _model_positions; diff --git a/meshers/blocky/voxel_library.cpp b/meshers/blocky/voxel_library.cpp index b80da22e..c7978d63 100644 --- a/meshers/blocky/voxel_library.cpp +++ b/meshers/blocky/voxel_library.cpp @@ -116,11 +116,6 @@ Ref VoxelLibrary::create_voxel(unsigned int id, String name) { return voxel; } -Ref VoxelLibrary::_b_get_voxel(unsigned int id) { - ERR_FAIL_COND_V(id >= _voxel_types.size(), Ref()); - return _voxel_types[id]; -} - void VoxelLibrary::_bind_methods() { ClassDB::bind_method(D_METHOD("create_voxel", "id", "name"), &VoxelLibrary::create_voxel); @@ -137,3 +132,8 @@ void VoxelLibrary::_bind_methods() { BIND_CONSTANT(MAX_VOXEL_TYPES); } + +Ref VoxelLibrary::_b_get_voxel(unsigned int id) { + ERR_FAIL_COND_V(id >= _voxel_types.size(), Ref()); + return _voxel_types[id]; +} diff --git a/meshers/blocky/voxel_library.h b/meshers/blocky/voxel_library.h index c809caf7..27d1ae2d 100644 --- a/meshers/blocky/voxel_library.h +++ b/meshers/blocky/voxel_library.h @@ -44,10 +44,11 @@ protected: Ref _b_get_voxel(unsigned int id); + void generate_side_culling_masks(); + private: std::vector > _voxel_types; int _atlas_size; }; #endif // VOXEL_LIBRARY_H - diff --git a/meshers/blocky/voxel_mesher_blocky.cpp b/meshers/blocky/voxel_mesher_blocky.cpp index 28d040bb..56902523 100644 --- a/meshers/blocky/voxel_mesher_blocky.cpp +++ b/meshers/blocky/voxel_mesher_blocky.cpp @@ -31,9 +31,9 @@ inline bool is_face_visible(const VoxelLibrary &lib, const Voxel &vt, int other_ if (other_vt.is_transparent() && vt.get_id() != other_voxel_id) { return true; } else { - const uint8_t m = vt.get_face_culling_mask(side); + const uint64_t m = vt.get_side_culling_mask(side); const int opposite_side = g_opposite_side[side]; - return (m & other_vt.get_face_culling_mask(opposite_side)) != m; + return (m & other_vt.get_side_culling_mask(opposite_side)) != m; } } return true;