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
master
Marc Gilleron 2020-02-08 23:05:44 +00:00
parent b2f7d23f7f
commit 7aa33267a8
5 changed files with 165 additions and 66 deletions

View File

@ -218,31 +218,32 @@ void Voxel::set_custom_mesh(Ref<Mesh> 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> mesh) {
FixedArray<HashMap<int, int>, Cube::SIDE_COUNT> added_side_indices;
HashMap<int, int> added_regular_indices;
FixedArray<Vector3, 3> 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> 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> 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> Voxel::set_cube_geometry(float sy) {
@ -356,15 +353,13 @@ Ref<Voxel> 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<Voxel>(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 <typename F>
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<Vector3> &positions = get_model_side_positions(side);
const std::vector<int> &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> Voxel::set_xquad_geometry(Vector2 atlas_pos) {
// // TODO
// return Ref<Voxel>(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);
}

View File

@ -47,6 +47,11 @@ public:
void set_custom_mesh(Ref<Mesh> mesh);
Ref<Mesh> 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<VoxelLibrary> 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<uint8_t, Cube::SIDE_COUNT> _side_culling_masks;
FixedArray<uint64_t, Cube::SIDE_COUNT> _side_culling_masks;
// Model
std::vector<Vector3> _model_positions;

View File

@ -116,11 +116,6 @@ Ref<Voxel> VoxelLibrary::create_voxel(unsigned int id, String name) {
return voxel;
}
Ref<Voxel> VoxelLibrary::_b_get_voxel(unsigned int id) {
ERR_FAIL_COND_V(id >= _voxel_types.size(), Ref<Voxel>());
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<Voxel> VoxelLibrary::_b_get_voxel(unsigned int id) {
ERR_FAIL_COND_V(id >= _voxel_types.size(), Ref<Voxel>());
return _voxel_types[id];
}

View File

@ -44,10 +44,11 @@ protected:
Ref<Voxel> _b_get_voxel(unsigned int id);
void generate_side_culling_masks();
private:
std::vector<Ref<Voxel> > _voxel_types;
int _atlas_size;
};
#endif // VOXEL_LIBRARY_H

View File

@ -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;