#include "voxel_mesher_blocky.h" #include "../../constants/cube_tables.h" #include "../../storage/voxel_buffer_internal.h" #include "../../util/godot/funcs.h" #include "../../util/math/conv.h" #include "../../util/span.h" #include namespace zylann::voxel { // 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 VoxelBlockyLibrary::BakedData &lib, const VoxelBlockyModel::BakedData &vt, uint32_t other_voxel_id, int side) { if (other_voxel_id < lib.models.size()) { const VoxelBlockyModel::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 VoxelBlockyLibrary::BakedData &lib, uint32_t voxel_id) { if (voxel_id < lib.models.size()) { const VoxelBlockyModel::BakedData &t = lib.models[voxel_id]; return t.contributes_to_ao; } return true; } static thread_local std::vector tls_index_offsets; } // namespace template void generate_blocky_mesh(std::vector &out_arrays_per_material, VoxelMesher::Output::CollisionSurface *collision_surface, const Span type_buffer, const Vector3i block_size, const VoxelBlockyLibrary::BakedData &library, bool bake_occlusion, float baked_occlusion_darkness) { // TODO Optimization: not sure if this mandates a template function. There is so much more happening in this // function other than reading voxels, although reading is on the hottest path. It needs to be profiled. If // changing makes no difference, we could use a function pointer or switch inside instead to reduce executable size. ERR_FAIL_COND(block_size.x < static_cast(2 * VoxelMesherBlocky::PADDING) || block_size.y < static_cast(2 * VoxelMesherBlocky::PADDING) || block_size.z < static_cast(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); std::vector &index_offsets = tls_index_offsets; index_offsets.clear(); index_offsets.resize(out_arrays_per_material.size(), 0); int collision_surface_index_offset = 0; FixedArray 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 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 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 == VoxelBlockyModel::AIR_ID || !library.has_model(voxel_id)) { continue; } const VoxelBlockyModel::BakedData &voxel = library.models[voxel_id]; const VoxelBlockyModel::BakedData::Model &model = voxel.model; // 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) { if ((model.empty_sides_mask & (1 << side)) != 0) { // This side is empty 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]; } } } } // Subtracting 1 because the data is padded Vector3f pos(x - 1, y - 1, z - 1); for (unsigned int surface_index = 0; surface_index < model.surface_count; ++surface_index) { const VoxelBlockyModel::BakedData::Surface &surface = model.surfaces[surface_index]; VoxelMesherBlocky::Arrays &arrays = out_arrays_per_material[surface.material_id]; ZN_ASSERT(surface.material_id >= 0 && surface.material_id < index_offsets.size()); int &index_offset = index_offsets[surface.material_id]; const std::vector &side_positions = surface.side_positions[side]; const unsigned int vertex_count = side_positions.size(); const std::vector &side_uvs = surface.side_uvs[side]; const std::vector &side_tangents = surface.side_tangents[side]; // 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); Vector3f *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(Vector2f)); } 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); Vector3f *w = arrays.normals.data() + append_index; for (unsigned int i = 0; i < vertex_count; ++i) { w[i] = to_vec3f(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) { Vector3f vertex_pos = 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(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(vertex_pos); 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 &side_indices = surface.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]; } } if (collision_surface != nullptr && surface.collision_enabled) { std::vector &dst_positions = collision_surface->positions; std::vector &dst_indices = collision_surface->indices; { const unsigned int append_index = dst_positions.size(); dst_positions.resize(dst_positions.size() + vertex_count); Vector3f *w = dst_positions.data() + append_index; for (unsigned int i = 0; i < vertex_count; ++i) { w[i] = side_positions[i] + pos; } } { int i = dst_indices.size(); dst_indices.resize(dst_indices.size() + index_count); int *w = dst_indices.data(); for (unsigned int j = 0; j < index_count; ++j) { w[i++] = collision_surface_index_offset + side_indices[j]; } } collision_surface_index_offset += vertex_count; } index_offset += vertex_count; } } // Inside for (unsigned int surface_index = 0; surface_index < model.surface_count; ++surface_index) { const VoxelBlockyModel::BakedData::Surface &surface = model.surfaces[surface_index]; if (surface.positions.size() == 0) { continue; } // TODO Get rid of push_backs VoxelMesherBlocky::Arrays &arrays = out_arrays_per_material[surface.material_id]; ZN_ASSERT(surface.material_id >= 0 && surface.material_id < index_offsets.size()); int &index_offset = index_offsets[surface.material_id]; const std::vector &positions = surface.positions; const unsigned int vertex_count = positions.size(); const Color modulate_color = voxel.color; const std::vector &normals = surface.normals; const std::vector &uvs = surface.uvs; const std::vector &tangents = surface.tangents; const Vector3f 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 &indices = surface.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]); } if (collision_surface != nullptr && surface.collision_enabled) { std::vector &dst_positions = collision_surface->positions; std::vector &dst_indices = collision_surface->indices; for (unsigned int i = 0; i < vertex_count; ++i) { dst_positions.push_back(positions[i] + pos); } for (unsigned int i = 0; i < index_count; ++i) { dst_indices.push_back(collision_surface_index_offset + indices[i]); } collision_surface_index_offset += vertex_count; } index_offset += vertex_count; } } } } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// thread_local VoxelMesherBlocky::Cache VoxelMesherBlocky::_cache; VoxelMesherBlocky::VoxelMesherBlocky() { set_padding(PADDING, PADDING); } VoxelMesherBlocky::~VoxelMesherBlocky() {} void VoxelMesherBlocky::set_library(Ref library) { RWLockWrite wlock(_parameters_lock); _parameters.library = library; } Ref 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 = math::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 = VoxelBufferInternal::CHANNEL_TYPE; Parameters params; { RWLockRead rlock(_parameters_lock); params = _parameters; } ERR_FAIL_COND(params.library.is_null()); Cache &cache = _cache; std::vector &arrays_per_material = cache.arrays_per_material; for (unsigned int i = 0; i < arrays_per_material.size(); ++i) { Arrays &a = 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 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); VoxelMesher::Output::CollisionSurface *collision_surface = nullptr; if (input.collision_hint) { collision_surface = &output.collision_surface; } unsigned int material_count = 0; { // We can only access baked data. Only this data is made for multithreaded access. RWLockRead lock(params.library->get_baked_data_rw_lock()); const VoxelBlockyLibrary::BakedData &library_baked_data = params.library->get_baked_data(); // There must be at least one "material" in case none of them are assigned. // This will produce a single surface, which will be rendered with a default material. material_count = math::max(library_baked_data.indexed_materials_count, 1u); if (arrays_per_material.size() < material_count) { arrays_per_material.resize(material_count); } switch (channel_depth) { case VoxelBufferInternal::DEPTH_8_BIT: generate_blocky_mesh(arrays_per_material, collision_surface, raw_channel, block_size, library_baked_data, params.bake_occlusion, baked_occlusion_darkness); break; case VoxelBufferInternal::DEPTH_16_BIT: generate_blocky_mesh(arrays_per_material, collision_surface, raw_channel.reinterpret_cast_to(), block_size, library_baked_data, params.bake_occlusion, baked_occlusion_darkness); break; default: ERR_PRINT("Unsupported voxel depth"); return; } output.surfaces.resize(material_count); } // TODO Optimization: we could return a single byte array and use Mesh::add_surface down the line? // That API does not seem to exist yet though. for (unsigned int i = 0; i < material_count; ++i) { const Arrays &arrays = 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; copy_to(positions, arrays.positions); copy_to(uvs, arrays.uvs); 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; } } ZN_ASSERT(i < output.surfaces.size()); Output::Surface &surface = output.surfaces[i]; surface.arrays = mesh_arrays; } // else { // // Empty // output.surfaces.push_back(Output::Surface()); // } } output.primitive_type = Mesh::PRIMITIVE_TRIANGLES; } Ref 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 << VoxelBufferInternal::CHANNEL_TYPE); } Ref VoxelMesherBlocky::get_material_by_index(unsigned int index) const { Ref lib = get_library(); if (lib.is_null()) { return Ref(); } return lib->get_material_by_index(index); } #ifdef TOOLS_ENABLED void VoxelMesherBlocky::get_configuration_warnings(TypedArray &out_warnings) const { Ref library = get_library(); if (library.is_null()) { out_warnings.append(TTR(VoxelMesherBlocky::get_class_static() + " has no " + VoxelBlockyLibrary::get_class_static() + " assigned.")); return; } if (library->get_voxel_count() == 0) { out_warnings.append(TTR("The " + VoxelBlockyLibrary::get_class_static() + " assigned to " + VoxelMesherBlocky::get_class_static() + " has an empty list of " + VoxelBlockyModel::get_class_static() + "s.")); return; } bool has_solid_model = false; for (unsigned int i = 0; i < library->get_voxel_count() && !has_solid_model; ++i) { const VoxelBlockyModel &model = library->get_voxel_const(i); switch (model.get_geometry_type()) { case VoxelBlockyModel::GEOMETRY_NONE: break; case VoxelBlockyModel::GEOMETRY_CUBE: has_solid_model = true; break; case VoxelBlockyModel::GEOMETRY_CUSTOM_MESH: has_solid_model = model.get_custom_mesh().is_valid(); break; default: ERR_PRINT("Unknown enum value"); break; } } if (!has_solid_model) { out_warnings.append(TTR("The " + VoxelBlockyLibrary::get_class_static() + " assigned to " + VoxelMesherBlocky::get_class_static() + " only has empty " + VoxelBlockyModel::get_class_static() + "s.")); } } #endif // TOOLS_ENABLED 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, VoxelBlockyLibrary::get_class_static(), 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"); } } // namespace zylann::voxel