godot_voxel/meshers/blocky/voxel_mesher_blocky.cpp
2021-12-29 17:06:24 +00:00

551 lines
20 KiB
C++

#include "voxel_mesher_blocky.h"
#include "../../constants/cube_tables.h"
#include "../../storage/voxel_buffer.h"
#include "../../util/funcs.h"
#include "../../util/span.h"
#include <core/os/os.h>
// Utility functions
namespace {
const int g_opposite_side[6] = {
Cube::SIDE_NEGATIVE_X, //
Cube::SIDE_POSITIVE_X, //
Cube::SIDE_POSITIVE_Y, //
Cube::SIDE_NEGATIVE_Y, //
Cube::SIDE_POSITIVE_Z, //
Cube::SIDE_NEGATIVE_Z //
};
inline bool is_face_visible(
const VoxelLibrary::BakedData &lib, const Voxel::BakedData &vt, uint32_t other_voxel_id, int side) {
if (other_voxel_id < lib.models.size()) {
const Voxel::BakedData &other_vt = lib.models[other_voxel_id];
if (other_vt.empty || (other_vt.transparency_index > vt.transparency_index)) {
return true;
} else {
const unsigned int ai = vt.model.side_pattern_indices[side];
const unsigned int bi = other_vt.model.side_pattern_indices[g_opposite_side[side]];
// Patterns are not the same, and B does not occlude A
return (ai != bi) && !lib.get_side_pattern_occlusion(bi, ai);
}
}
return true;
}
inline bool contributes_to_ao(const VoxelLibrary::BakedData &lib, uint32_t voxel_id) {
if (voxel_id < lib.models.size()) {
const Voxel::BakedData &t = lib.models[voxel_id];
return t.contributes_to_ao;
}
return true;
}
} // namespace
template <typename Type_T>
static void generate_blocky_mesh(
FixedArray<VoxelMesherBlocky::Arrays, VoxelMesherBlocky::MAX_MATERIALS> &out_arrays_per_material,
const Span<Type_T> type_buffer, const Vector3i block_size, const VoxelLibrary::BakedData &library,
bool bake_occlusion, float baked_occlusion_darkness) {
ERR_FAIL_COND(block_size.x < static_cast<int>(2 * VoxelMesherBlocky::PADDING) ||
block_size.y < static_cast<int>(2 * VoxelMesherBlocky::PADDING) ||
block_size.z < static_cast<int>(2 * VoxelMesherBlocky::PADDING));
// Build lookup tables so to speed up voxel access.
// These are values to add to an address in order to get given neighbor.
const int row_size = block_size.y;
const int deck_size = block_size.x * row_size;
// Data must be padded, hence the off-by-one
const Vector3i min = Vector3iUtil::create(VoxelMesherBlocky::PADDING);
const Vector3i max = block_size - Vector3iUtil::create(VoxelMesherBlocky::PADDING);
int index_offsets[VoxelMesherBlocky::MAX_MATERIALS] = { 0 };
FixedArray<int, Cube::SIDE_COUNT> side_neighbor_lut;
side_neighbor_lut[Cube::SIDE_LEFT] = row_size;
side_neighbor_lut[Cube::SIDE_RIGHT] = -row_size;
side_neighbor_lut[Cube::SIDE_BACK] = -deck_size;
side_neighbor_lut[Cube::SIDE_FRONT] = deck_size;
side_neighbor_lut[Cube::SIDE_BOTTOM] = -1;
side_neighbor_lut[Cube::SIDE_TOP] = 1;
FixedArray<int, Cube::EDGE_COUNT> edge_neighbor_lut;
edge_neighbor_lut[Cube::EDGE_BOTTOM_BACK] =
side_neighbor_lut[Cube::SIDE_BOTTOM] + side_neighbor_lut[Cube::SIDE_BACK];
edge_neighbor_lut[Cube::EDGE_BOTTOM_FRONT] =
side_neighbor_lut[Cube::SIDE_BOTTOM] + side_neighbor_lut[Cube::SIDE_FRONT];
edge_neighbor_lut[Cube::EDGE_BOTTOM_LEFT] =
side_neighbor_lut[Cube::SIDE_BOTTOM] + side_neighbor_lut[Cube::SIDE_LEFT];
edge_neighbor_lut[Cube::EDGE_BOTTOM_RIGHT] =
side_neighbor_lut[Cube::SIDE_BOTTOM] + side_neighbor_lut[Cube::SIDE_RIGHT];
edge_neighbor_lut[Cube::EDGE_BACK_LEFT] = side_neighbor_lut[Cube::SIDE_BACK] + side_neighbor_lut[Cube::SIDE_LEFT];
edge_neighbor_lut[Cube::EDGE_BACK_RIGHT] = side_neighbor_lut[Cube::SIDE_BACK] + side_neighbor_lut[Cube::SIDE_RIGHT];
edge_neighbor_lut[Cube::EDGE_FRONT_LEFT] = side_neighbor_lut[Cube::SIDE_FRONT] + side_neighbor_lut[Cube::SIDE_LEFT];
edge_neighbor_lut[Cube::EDGE_FRONT_RIGHT] =
side_neighbor_lut[Cube::SIDE_FRONT] + side_neighbor_lut[Cube::SIDE_RIGHT];
edge_neighbor_lut[Cube::EDGE_TOP_BACK] = side_neighbor_lut[Cube::SIDE_TOP] + side_neighbor_lut[Cube::SIDE_BACK];
edge_neighbor_lut[Cube::EDGE_TOP_FRONT] = side_neighbor_lut[Cube::SIDE_TOP] + side_neighbor_lut[Cube::SIDE_FRONT];
edge_neighbor_lut[Cube::EDGE_TOP_LEFT] = side_neighbor_lut[Cube::SIDE_TOP] + side_neighbor_lut[Cube::SIDE_LEFT];
edge_neighbor_lut[Cube::EDGE_TOP_RIGHT] = side_neighbor_lut[Cube::SIDE_TOP] + side_neighbor_lut[Cube::SIDE_RIGHT];
FixedArray<int, Cube::CORNER_COUNT> corner_neighbor_lut;
corner_neighbor_lut[Cube::CORNER_BOTTOM_BACK_LEFT] = side_neighbor_lut[Cube::SIDE_BOTTOM] +
side_neighbor_lut[Cube::SIDE_BACK] + side_neighbor_lut[Cube::SIDE_LEFT];
corner_neighbor_lut[Cube::CORNER_BOTTOM_BACK_RIGHT] = side_neighbor_lut[Cube::SIDE_BOTTOM] +
side_neighbor_lut[Cube::SIDE_BACK] + side_neighbor_lut[Cube::SIDE_RIGHT];
corner_neighbor_lut[Cube::CORNER_BOTTOM_FRONT_RIGHT] = side_neighbor_lut[Cube::SIDE_BOTTOM] +
side_neighbor_lut[Cube::SIDE_FRONT] + side_neighbor_lut[Cube::SIDE_RIGHT];
corner_neighbor_lut[Cube::CORNER_BOTTOM_FRONT_LEFT] = side_neighbor_lut[Cube::SIDE_BOTTOM] +
side_neighbor_lut[Cube::SIDE_FRONT] + side_neighbor_lut[Cube::SIDE_LEFT];
corner_neighbor_lut[Cube::CORNER_TOP_BACK_LEFT] =
side_neighbor_lut[Cube::SIDE_TOP] + side_neighbor_lut[Cube::SIDE_BACK] + side_neighbor_lut[Cube::SIDE_LEFT];
corner_neighbor_lut[Cube::CORNER_TOP_BACK_RIGHT] = side_neighbor_lut[Cube::SIDE_TOP] +
side_neighbor_lut[Cube::SIDE_BACK] + side_neighbor_lut[Cube::SIDE_RIGHT];
corner_neighbor_lut[Cube::CORNER_TOP_FRONT_RIGHT] = side_neighbor_lut[Cube::SIDE_TOP] +
side_neighbor_lut[Cube::SIDE_FRONT] + side_neighbor_lut[Cube::SIDE_RIGHT];
corner_neighbor_lut[Cube::CORNER_TOP_FRONT_LEFT] = side_neighbor_lut[Cube::SIDE_TOP] +
side_neighbor_lut[Cube::SIDE_FRONT] + side_neighbor_lut[Cube::SIDE_LEFT];
//uint64_t time_prep = Time::get_singleton()->get_ticks_usec() - time_before;
//time_before = Time::get_singleton()->get_ticks_usec();
for (unsigned int z = min.z; z < (unsigned int)max.z; ++z) {
for (unsigned int x = min.x; x < (unsigned int)max.x; ++x) {
for (unsigned int y = min.y; y < (unsigned int)max.y; ++y) {
// min and max are chosen such that you can visit 1 neighbor away from the current voxel without size
// check
const int voxel_index = y + x * row_size + z * deck_size;
const int voxel_id = type_buffer[voxel_index];
if (voxel_id != 0 && library.has_model(voxel_id)) {
const Voxel::BakedData &voxel = library.models[voxel_id];
VoxelMesherBlocky::Arrays &arrays = out_arrays_per_material[voxel.material_id];
int &index_offset = index_offsets[voxel.material_id];
// Hybrid approach: extract cube faces and decimate those that aren't visible,
// and still allow voxels to have geometry that is not a cube
// Sides
for (unsigned int side = 0; side < Cube::SIDE_COUNT; ++side) {
const std::vector<Vector3> &side_positions = voxel.model.side_positions[side];
const unsigned int vertex_count = side_positions.size();
if (vertex_count == 0) {
continue;
}
const uint32_t neighbor_voxel_id = type_buffer[voxel_index + side_neighbor_lut[side]];
if (!is_face_visible(library, voxel, neighbor_voxel_id, side)) {
continue;
}
// The face is visible
int shaded_corner[8] = { 0 };
if (bake_occlusion) {
// Combinatory solution for
// https://0fps.net/2013/07/03/ambient-occlusion-for-minecraft-like-worlds/ (inverted)
// function vertexAO(side1, side2, corner) {
// if(side1 && side2) {
// return 0
// }
// return 3 - (side1 + side2 + corner)
// }
for (unsigned int j = 0; j < 4; ++j) {
const unsigned int edge = Cube::g_side_edges[side][j];
const int edge_neighbor_id = type_buffer[voxel_index + edge_neighbor_lut[edge]];
if (contributes_to_ao(library, edge_neighbor_id)) {
++shaded_corner[Cube::g_edge_corners[edge][0]];
++shaded_corner[Cube::g_edge_corners[edge][1]];
}
}
for (unsigned int j = 0; j < 4; ++j) {
const unsigned int corner = Cube::g_side_corners[side][j];
if (shaded_corner[corner] == 2) {
shaded_corner[corner] = 3;
} else {
const int corner_neigbor_id =
type_buffer[voxel_index + corner_neighbor_lut[corner]];
if (contributes_to_ao(library, corner_neigbor_id)) {
++shaded_corner[corner];
}
}
}
}
const std::vector<Vector2> &side_uvs = voxel.model.side_uvs[side];
const std::vector<float> &side_tangents = voxel.model.side_tangents[side];
// Subtracting 1 because the data is padded
Vector3 pos(x - 1, y - 1, z - 1);
// Append vertices of the faces in one go, don't use push_back
{
const int append_index = arrays.positions.size();
arrays.positions.resize(arrays.positions.size() + vertex_count);
Vector3 *w = arrays.positions.data() + append_index;
for (unsigned int i = 0; i < vertex_count; ++i) {
w[i] = side_positions[i] + pos;
}
}
{
const int append_index = arrays.uvs.size();
arrays.uvs.resize(arrays.uvs.size() + vertex_count);
memcpy(arrays.uvs.data() + append_index, side_uvs.data(), vertex_count * sizeof(Vector2));
}
if (side_tangents.size() > 0) {
const int append_index = arrays.tangents.size();
arrays.tangents.resize(arrays.tangents.size() + vertex_count * 4);
memcpy(arrays.tangents.data() + append_index, side_tangents.data(),
(vertex_count * 4) * sizeof(float));
}
{
const int append_index = arrays.normals.size();
arrays.normals.resize(arrays.normals.size() + vertex_count);
Vector3 *w = arrays.normals.data() + append_index;
for (unsigned int i = 0; i < vertex_count; ++i) {
w[i] = Cube::g_side_normals[side];
}
}
{
const int append_index = arrays.colors.size();
arrays.colors.resize(arrays.colors.size() + vertex_count);
Color *w = arrays.colors.data() + append_index;
const Color modulate_color = voxel.color;
if (bake_occlusion) {
for (unsigned int i = 0; i < vertex_count; ++i) {
Vector3 v = side_positions[i];
// General purpose occlusion colouring.
// TODO Optimize for cubes
// TODO Fix occlusion inconsistency caused by triangles orientation? Not sure if
// worth it
float shade = 0;
for (unsigned int j = 0; j < 4; ++j) {
unsigned int corner = Cube::g_side_corners[side][j];
if (shaded_corner[corner]) {
float s = baked_occlusion_darkness *
static_cast<float>(shaded_corner[corner]);
//float k = 1.f - Cube::g_corner_position[corner].distance_to(v);
float k = 1.f - Cube::g_corner_position[corner].distance_squared_to(v);
if (k < 0.0) {
k = 0.0;
}
s *= k;
if (s > shade) {
shade = s;
}
}
}
const float gs = 1.0 - shade;
w[i] = Color(gs, gs, gs) * modulate_color;
}
} else {
for (unsigned int i = 0; i < vertex_count; ++i) {
w[i] = modulate_color;
}
}
}
const std::vector<int> &side_indices = voxel.model.side_indices[side];
const unsigned int index_count = side_indices.size();
{
int i = arrays.indices.size();
arrays.indices.resize(arrays.indices.size() + index_count);
int *w = arrays.indices.data();
for (unsigned int j = 0; j < index_count; ++j) {
w[i++] = index_offset + side_indices[j];
}
}
index_offset += vertex_count;
}
// Inside
if (voxel.model.positions.size() != 0) {
// TODO Get rid of push_backs
const std::vector<Vector3> &positions = voxel.model.positions;
const unsigned int vertex_count = positions.size();
const Color modulate_color = voxel.color;
const std::vector<Vector3> &normals = voxel.model.normals;
const std::vector<Vector2> &uvs = voxel.model.uvs;
const std::vector<float> &tangents = voxel.model.tangents;
const Vector3 pos(x - 1, y - 1, z - 1);
if (tangents.size() > 0) {
const int append_index = arrays.tangents.size();
arrays.tangents.resize(arrays.tangents.size() + vertex_count * 4);
memcpy(arrays.tangents.data() + append_index, tangents.data(),
(vertex_count * 4) * sizeof(float));
}
for (unsigned int i = 0; i < vertex_count; ++i) {
arrays.normals.push_back(normals[i]);
arrays.uvs.push_back(uvs[i]);
arrays.positions.push_back(positions[i] + pos);
// TODO handle ambient occlusion on inner parts
arrays.colors.push_back(modulate_color);
}
const std::vector<int> &indices = voxel.model.indices;
const unsigned int index_count = indices.size();
for (unsigned int i = 0; i < index_count; ++i) {
arrays.indices.push_back(index_offset + indices[i]);
}
index_offset += vertex_count;
}
}
}
}
}
}
thread_local VoxelMesherBlocky::Cache VoxelMesherBlocky::_cache;
VoxelMesherBlocky::VoxelMesherBlocky() {
set_padding(PADDING, PADDING);
}
VoxelMesherBlocky::~VoxelMesherBlocky() {}
void VoxelMesherBlocky::set_library(Ref<VoxelLibrary> library) {
RWLockWrite wlock(_parameters_lock);
_parameters.library = library;
}
Ref<VoxelLibrary> VoxelMesherBlocky::get_library() const {
RWLockRead rlock(_parameters_lock);
return _parameters.library;
}
void VoxelMesherBlocky::set_occlusion_darkness(float darkness) {
RWLockWrite wlock(_parameters_lock);
_parameters.baked_occlusion_darkness = clamp(darkness, 0.0f, 1.0f);
}
float VoxelMesherBlocky::get_occlusion_darkness() const {
RWLockRead rlock(_parameters_lock);
return _parameters.baked_occlusion_darkness;
}
void VoxelMesherBlocky::set_occlusion_enabled(bool enable) {
RWLockWrite wlock(_parameters_lock);
_parameters.bake_occlusion = enable;
}
bool VoxelMesherBlocky::get_occlusion_enabled() const {
RWLockRead rlock(_parameters_lock);
return _parameters.bake_occlusion;
}
void VoxelMesherBlocky::build(VoxelMesher::Output &output, const VoxelMesher::Input &input) {
const int channel = VoxelBuffer::CHANNEL_TYPE;
Parameters params;
{
RWLockRead rlock(_parameters_lock);
params = _parameters;
}
ERR_FAIL_COND(params.library.is_null());
Cache &cache = _cache;
for (unsigned int i = 0; i < cache.arrays_per_material.size(); ++i) {
Arrays &a = cache.arrays_per_material[i];
a.clear();
}
float baked_occlusion_darkness = 0;
if (params.bake_occlusion) {
baked_occlusion_darkness = params.baked_occlusion_darkness / 3.0f;
}
// The technique is Culled faces.
// Could be improved with greedy meshing: https://0fps.net/2012/06/30/meshing-in-a-minecraft-game/
// However I don't feel it's worth it yet:
// - Not so much gain for organic worlds with lots of texture variations
// - Works well with cubes but not with any shape
// - Slower
// => Could be implemented in a separate class?
const VoxelBufferInternal &voxels = input.voxels;
#ifdef TOOLS_ENABLED
if (input.lod != 0) {
WARN_PRINT("VoxelMesherBlocky received lod != 0, it is not supported");
}
#endif
// Iterate 3D padded data to extract voxel faces.
// This is the most intensive job in this class, so all required data should be as fit as possible.
// The buffer we receive MUST be dense (i.e not compressed, and channels allocated).
// That means we can use raw pointers to voxel data inside instead of using the higher-level getters,
// and then save a lot of time.
if (voxels.get_channel_compression(channel) == VoxelBufferInternal::COMPRESSION_UNIFORM) {
// All voxels have the same type.
// If it's all air, nothing to do. If it's all cubes, nothing to do either.
// TODO Handle edge case of uniform block with non-cubic voxels!
// If the type of voxel still produces geometry in this situation (which is an absurd use case but not an
// error), decompress into a backing array to still allow the use of the same algorithm.
return;
} else if (voxels.get_channel_compression(channel) != VoxelBufferInternal::COMPRESSION_NONE) {
// No other form of compression is allowed
ERR_PRINT("VoxelMesherBlocky received unsupported voxel compression");
return;
}
Span<uint8_t> raw_channel;
if (!voxels.get_channel_raw(channel, raw_channel)) {
/* _
// | \
// /\ \\
// / /|\\\
// | |\ \\\
// | \_\ \\|
// | | )
// \ | |
// \ /
*/
// Case supposedly handled before...
ERR_PRINT("Something wrong happened");
return;
}
const Vector3i block_size = voxels.get_size();
const VoxelBufferInternal::Depth channel_depth = voxels.get_channel_depth(channel);
{
// We can only access baked data. Only this data is made for multithreaded access.
RWLockRead lock(params.library->get_baked_data_rw_lock());
const VoxelLibrary::BakedData &library_baked_data = params.library->get_baked_data();
switch (channel_depth) {
case VoxelBufferInternal::DEPTH_8_BIT:
generate_blocky_mesh(cache.arrays_per_material, raw_channel, block_size, library_baked_data,
params.bake_occlusion, baked_occlusion_darkness);
break;
case VoxelBufferInternal::DEPTH_16_BIT:
generate_blocky_mesh(cache.arrays_per_material, raw_channel.reinterpret_cast_to<uint16_t>(), block_size,
library_baked_data, params.bake_occlusion, baked_occlusion_darkness);
break;
default:
ERR_PRINT("Unsupported voxel depth");
return;
}
}
// TODO We could return a single byte array and use Mesh::add_surface down the line?
for (unsigned int i = 0; i < MAX_MATERIALS; ++i) {
const Arrays &arrays = cache.arrays_per_material[i];
if (arrays.positions.size() != 0) {
Array mesh_arrays;
mesh_arrays.resize(Mesh::ARRAY_MAX);
{
PackedVector3Array positions;
PackedVector2Array uvs;
PackedVector3Array normals;
PackedColorArray colors;
PackedInt32Array indices;
raw_copy_to(positions, arrays.positions);
raw_copy_to(uvs, arrays.uvs);
raw_copy_to(normals, arrays.normals);
raw_copy_to(colors, arrays.colors);
raw_copy_to(indices, arrays.indices);
mesh_arrays[Mesh::ARRAY_VERTEX] = positions;
mesh_arrays[Mesh::ARRAY_TEX_UV] = uvs;
mesh_arrays[Mesh::ARRAY_NORMAL] = normals;
mesh_arrays[Mesh::ARRAY_COLOR] = colors;
mesh_arrays[Mesh::ARRAY_INDEX] = indices;
if (arrays.tangents.size() > 0) {
PackedFloat32Array tangents;
raw_copy_to(tangents, arrays.tangents);
mesh_arrays[Mesh::ARRAY_TANGENT] = tangents;
}
}
output.surfaces.push_back(mesh_arrays);
} else {
// Empty
output.surfaces.push_back(Array());
}
}
output.primitive_type = Mesh::PRIMITIVE_TRIANGLES;
}
Ref<Resource> VoxelMesherBlocky::duplicate(bool p_subresources) const {
Parameters params;
{
RWLockRead rlock(_parameters_lock);
params = _parameters;
}
if (p_subresources && params.library.is_valid()) {
params.library = params.library->duplicate(true);
}
VoxelMesherBlocky *c = memnew(VoxelMesherBlocky);
c->_parameters = params;
return c;
}
int VoxelMesherBlocky::get_used_channels_mask() const {
return (1 << VoxelBuffer::CHANNEL_TYPE);
}
void VoxelMesherBlocky::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_library", "voxel_library"), &VoxelMesherBlocky::set_library);
ClassDB::bind_method(D_METHOD("get_library"), &VoxelMesherBlocky::get_library);
ClassDB::bind_method(D_METHOD("set_occlusion_enabled", "enable"), &VoxelMesherBlocky::set_occlusion_enabled);
ClassDB::bind_method(D_METHOD("get_occlusion_enabled"), &VoxelMesherBlocky::get_occlusion_enabled);
ClassDB::bind_method(D_METHOD("set_occlusion_darkness", "value"), &VoxelMesherBlocky::set_occlusion_darkness);
ClassDB::bind_method(D_METHOD("get_occlusion_darkness"), &VoxelMesherBlocky::get_occlusion_darkness);
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "library", PROPERTY_HINT_RESOURCE_TYPE, "VoxelLibrary",
PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_EDITOR_INSTANTIATE_OBJECT),
"set_library", "get_library");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "occlusion_enabled"), "set_occlusion_enabled", "get_occlusion_enabled");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "occlusion_darkness", PROPERTY_HINT_RANGE, "0,1,0.01"),
"set_occlusion_darkness", "get_occlusion_darkness");
}