#include "voxel.h" #include "../../util/macros.h" #include "voxel_library.h" #include "voxel_mesher_blocky.h" // TODO Only required because of MAX_MATERIALS... could be enough inverting that dependency #define STRLEN(x) (sizeof(x) / sizeof(x[0])) Voxel::Voxel() : _id(-1), _material_id(0), _transparency_index(0), _color(1.f, 1.f, 1.f), _geometry_type(GEOMETRY_NONE) { } static Cube::Side name_to_side(const String &s) { if (s == "left") { return Cube::SIDE_LEFT; } if (s == "right") { return Cube::SIDE_RIGHT; } if (s == "top") { return Cube::SIDE_TOP; } if (s == "bottom") { return Cube::SIDE_BOTTOM; } if (s == "front") { return Cube::SIDE_FRONT; } if (s == "back") { return Cube::SIDE_BACK; } return Cube::SIDE_COUNT; // Invalid } bool Voxel::_set(const StringName &p_name, const Variant &p_value) { String name = p_name; // TODO Eventualy these could be Rect2 for maximum flexibility? if (name.begins_with("cube_tiles/")) { String s = name.substr(STRLEN("cube_tiles/") - 1, name.length()); Cube::Side side = name_to_side(s); if (side != Cube::SIDE_COUNT) { Vector2 v = p_value; set_cube_uv_side(side, v); return true; } } return false; } bool Voxel::_get(const StringName &p_name, Variant &r_ret) const { String name = p_name; if (name.begins_with("cube_tiles/")) { String s = name.substr(STRLEN("cube_tiles/") - 1, name.length()); Cube::Side side = name_to_side(s); if (side != Cube::SIDE_COUNT) { r_ret = _cube_tiles[side]; return true; } } return false; } void Voxel::_get_property_list(List *p_list) const { if (_geometry_type == GEOMETRY_CUBE) { p_list->push_back(PropertyInfo(Variant::REAL, "cube_geometry/padding_y")); p_list->push_back(PropertyInfo(Variant::VECTOR2, "cube_tiles/left")); p_list->push_back(PropertyInfo(Variant::VECTOR2, "cube_tiles/right")); p_list->push_back(PropertyInfo(Variant::VECTOR2, "cube_tiles/bottom")); p_list->push_back(PropertyInfo(Variant::VECTOR2, "cube_tiles/top")); p_list->push_back(PropertyInfo(Variant::VECTOR2, "cube_tiles/back")); p_list->push_back(PropertyInfo(Variant::VECTOR2, "cube_tiles/front")); } } void Voxel::set_voxel_name(String name) { _name = name; } void Voxel::set_id(int id) { ERR_FAIL_COND(id < 0 || (unsigned int)id >= VoxelLibrary::MAX_VOXEL_TYPES); // Cannot modify ID after creation ERR_FAIL_COND_MSG(_id != -1, "ID cannot be modified after being added to a library"); _id = id; } void Voxel::set_color(Color color) { _color = color; } void Voxel::set_material_id(unsigned int id) { ERR_FAIL_COND(id >= VoxelMesherBlocky::MAX_MATERIALS); _material_id = id; } void Voxel::set_transparent(bool t) { if (t) { if (_transparency_index == 0) { _transparency_index = 1; } } else { _transparency_index = 0; } } void Voxel::set_transparency_index(int i) { _transparency_index = clamp(i, 0, 255); } void Voxel::set_geometry_type(GeometryType type) { _geometry_type = type; switch (_geometry_type) { case GEOMETRY_NONE: _collision_aabbs.clear(); break; case GEOMETRY_CUBE: _collision_aabbs.clear(); _collision_aabbs.push_back(AABB(Vector3(0, 0, 0), Vector3(1, 1, 1))); _empty = false; break; case GEOMETRY_CUSTOM_MESH: // Gotta be user-defined break; default: ERR_PRINT("Wtf? Unknown geometry type"); break; } } Voxel::GeometryType Voxel::get_geometry_type() const { return _geometry_type; } void Voxel::set_custom_mesh(Ref mesh) { _custom_mesh = mesh; } void Voxel::set_cube_geometry() { } void Voxel::set_random_tickable(bool rt) { _random_tickable = rt; } void Voxel::set_cube_uv_side(int side, Vector2 tile_pos) { _cube_tiles[side] = tile_pos; } Ref Voxel::duplicate(bool p_subresources) const { Ref d_ref; d_ref.instance(); Voxel &d = **d_ref; d._id = -1; d._name = _name; d._material_id = _material_id; d._transparency_index = _transparency_index; d._color = _color; d._geometry_type = _geometry_type; d._cube_tiles = _cube_tiles; d._custom_mesh = _custom_mesh; d._collision_aabbs = _collision_aabbs; d._random_tickable = _random_tickable; d._empty = _empty; if (p_subresources) { if (d._custom_mesh.is_valid()) { d._custom_mesh = d._custom_mesh->duplicate(p_subresources); } } return d_ref; } void Voxel::set_collision_mask(uint32_t mask) { _collision_mask = mask; } static void bake_cube_geometry(Voxel &config, Voxel::BakedData &baked_data, int p_atlas_size, bool bake_tangents) { const float sy = 1.0; for (unsigned int side = 0; side < Cube::SIDE_COUNT; ++side) { std::vector &positions = baked_data.model.side_positions[side]; positions.resize(4); for (unsigned int i = 0; i < 4; ++i) { int corner = Cube::g_side_corners[side][i]; Vector3 p = Cube::g_corner_position[corner]; if (p.y > 0.9) { p.y = sy; } positions[i] = p; } std::vector &indices = baked_data.model.side_indices[side]; indices.resize(6); for (unsigned int i = 0; i < 6; ++i) { indices[i] = Cube::g_side_quad_triangles[side][i]; } } const float e = 0.001; // Winding is the same as the one chosen in Cube:: vertices // I am confused. I read in at least 3 OpenGL tutorials that texture coordinates start at bottom-left (0,0). // But even though Godot is said to follow OpenGL's convention, the engine starts at top-left! const Vector2 uv[4] = { Vector2(e, 1.f - e), Vector2(1.f - e, 1.f - e), Vector2(1.f - e, e), Vector2(e, e), }; const float atlas_size = (float)p_atlas_size; CRASH_COND(atlas_size <= 0); const float s = 1.0 / atlas_size; for (unsigned int side = 0; side < Cube::SIDE_COUNT; ++side) { baked_data.model.side_uvs[side].resize(4); std::vector &uvs = baked_data.model.side_uvs[side]; for (unsigned int i = 0; i < 4; ++i) { uvs[i] = (config.get_cube_tile(side) + uv[i]) * s; } if(bake_tangents){ std::vector &tangents = baked_data.model.side_tangents[side]; for (unsigned int i = 0; i < 4; ++i) { for (unsigned int j = 0; j < 4; ++j) tangents.push_back(Cube::g_side_tangents[side][j]); } } } baked_data.empty = false; } static void bake_mesh_geometry(Voxel &config, Voxel::BakedData &baked_data, bool bake_tangents) { Ref mesh = config.get_custom_mesh(); if (mesh.is_null()) { baked_data.empty = true; return; } Array arrays = mesh->surface_get_arrays(0); ERR_FAIL_COND(arrays.size() == 0); PoolIntArray indices = arrays[Mesh::ARRAY_INDEX]; ERR_FAIL_COND_MSG(indices.size() % 3 != 0, "Mesh is empty or does not contain triangles"); PoolVector3Array positions = arrays[Mesh::ARRAY_VERTEX]; PoolVector3Array normals = arrays[Mesh::ARRAY_NORMAL]; PoolVector2Array uvs = arrays[Mesh::ARRAY_TEX_UV]; PoolVector tangents = arrays[Mesh::ARRAY_TANGENT]; baked_data.empty = positions.size() == 0; ERR_FAIL_COND(normals.size() == 0); struct L { 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; } 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; } }; if (uvs.size() == 0) { // TODO Properly generate UVs if there arent any uvs = PoolVector2Array(); uvs.resize(positions.size()); } bool tangents_empty = tangents.size() == 0 ? false : true; // Separate triangles belonging to faces of the cube { PoolIntArray::Read indices_read = indices.read(); PoolVector3Array::Read positions_read = positions.read(); PoolVector3Array::Read normals_read = normals.read(); PoolVector2Array::Read uvs_read = uvs.read(); PoolVector::Read tangents_read = tangents.read(); FixedArray, Cube::SIDE_COUNT> added_side_indices; HashMap added_regular_indices; FixedArray tri_positions; Voxel::BakedData::Model &model = baked_data.model; for (int i = 0; i < indices.size(); i += 3) { Cube::SideAxis side; 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]]; float tangent[4]; if(tangents_empty && bake_tangents){ //If tangents are empty then we calculate them Vector2 delta_uv1 = uvs_read[indices_read[i+1]] - uvs_read[indices_read[i]]; Vector2 delta_uv2 = uvs_read[indices_read[i+2]] - uvs_read[indices_read[i]]; Vector3 delta_pos1 = tri_positions[1] - tri_positions[0]; Vector3 delta_pos2 = tri_positions[2] - tri_positions[0]; float r = 1.0f / (delta_uv1[0] * delta_uv2[1] - delta_uv1[1] * delta_uv2[0]); Vector3 t = (delta_pos1 * delta_uv2[1] - delta_pos2 * delta_uv1[1])*r; Vector3 bt = (delta_pos2 * delta_uv1[0] - delta_pos1 * delta_uv2[0])*r; tangent[0] = t[0]; tangent[1] = t[1]; tangent[2] = t[2]; tangent[3] = (bt.dot(normals_read[indices_read[i]].cross(t))) < 0 ? -1.0f : 1.0f; } 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[side].size(); for (int j = 0; j < 3; ++j) { int src_index = indices_read[i + j]; const int *existing_dst_index = added_side_indices[side].getptr(src_index); if (existing_dst_index == nullptr) { // Add new vertex 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]]); if(tangents_empty && bake_tangents) { model.side_tangents[side].push_back(tangent[0]); model.side_tangents[side].push_back(tangent[1]); model.side_tangents[side].push_back(tangent[2]); model.side_tangents[side].push_back(tangent[3]); } else if(bake_tangents) { int ti = (i / 3) * 4; // i is the first vertex of each triangle which increments in 3s. There are 4 floats per tangent. model.side_tangents[side].push_back(tangents_read[ti]); model.side_tangents[side].push_back(tangents_read[ti+1]); model.side_tangents[side].push_back(tangents_read[ti+2]); model.side_tangents[side].push_back(tangents_read[ti+3]); } 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[side].push_back(*existing_dst_index); } } } else { // That triangle is not on the face int next_regular_index = model.positions.size(); for (int j = 0; j < 3; ++j) { int src_index = indices_read[i + j]; const int *existing_dst_index = added_regular_indices.getptr(src_index); if (existing_dst_index == nullptr) { model.indices.push_back(next_regular_index); 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]]); if(tangents_empty && bake_tangents){ model.tangents.push_back(tangent[0]); model.tangents.push_back(tangent[1]); model.tangents.push_back(tangent[2]); model.tangents.push_back(tangent[3]); } else if(bake_tangents) { int ti = (i / 3) * 4; // i is the first vertex of each triangle which increments in 3s. There are 4 floats per tangent. model.tangents.push_back(tangents_read[ti]); model.tangents.push_back(tangents_read[ti+1]); model.tangents.push_back(tangents_read[ti+2]); model.tangents.push_back(tangents_read[ti+3]); } added_regular_indices.set(src_index, next_regular_index); ++next_regular_index; } else { model.indices.push_back(*existing_dst_index); } } } } } } void Voxel::bake(BakedData &baked_data, int p_atlas_size, bool bake_tangents) { baked_data.clear(); // baked_data.contributes_to_ao is set by the side culling phase baked_data.transparency_index = _transparency_index; baked_data.material_id = _material_id; baked_data.color = _color; switch (_geometry_type) { case GEOMETRY_NONE: baked_data.empty = true; break; case GEOMETRY_CUBE: bake_cube_geometry(*this, baked_data, p_atlas_size, bake_tangents); break; case GEOMETRY_CUSTOM_MESH: bake_mesh_geometry(*this, baked_data, bake_tangents); break; default: ERR_PRINT("Wtf? Unknown geometry type"); break; } _empty = baked_data.empty; } void Voxel::_bind_methods() { ClassDB::bind_method(D_METHOD("set_voxel_name", "name"), &Voxel::set_voxel_name); ClassDB::bind_method(D_METHOD("get_voxel_name"), &Voxel::get_voxel_name); ClassDB::bind_method(D_METHOD("set_id", "id"), &Voxel::set_id); ClassDB::bind_method(D_METHOD("get_id"), &Voxel::get_id); ClassDB::bind_method(D_METHOD("set_color", "color"), &Voxel::set_color); ClassDB::bind_method(D_METHOD("get_color"), &Voxel::get_color); ClassDB::bind_method(D_METHOD("set_transparent", "transparent"), &Voxel::set_transparent); ClassDB::bind_method(D_METHOD("is_transparent"), &Voxel::is_transparent); ClassDB::bind_method(D_METHOD("set_transparency_index", "transparency_index"), &Voxel::set_transparency_index); ClassDB::bind_method(D_METHOD("get_transparency_index"), &Voxel::get_transparency_index); ClassDB::bind_method(D_METHOD("set_random_tickable", "rt"), &Voxel::set_random_tickable); ClassDB::bind_method(D_METHOD("is_random_tickable"), &Voxel::is_random_tickable); ClassDB::bind_method(D_METHOD("set_material_id", "id"), &Voxel::set_material_id); ClassDB::bind_method(D_METHOD("get_material_id"), &Voxel::get_material_id); ClassDB::bind_method(D_METHOD("set_geometry_type", "type"), &Voxel::set_geometry_type); ClassDB::bind_method(D_METHOD("get_geometry_type"), &Voxel::get_geometry_type); ClassDB::bind_method(D_METHOD("set_custom_mesh", "type"), &Voxel::set_custom_mesh); ClassDB::bind_method(D_METHOD("get_custom_mesh"), &Voxel::get_custom_mesh); 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_collision_mask", "mask"), &Voxel::set_collision_mask); ClassDB::bind_method(D_METHOD("get_collision_mask"), &Voxel::get_collision_mask); ClassDB::bind_method(D_METHOD("is_empty"), &Voxel::is_empty); // TODO Update to StringName in Godot 4 ADD_PROPERTY(PropertyInfo(Variant::STRING, "voxel_name"), "set_voxel_name", "get_voxel_name"); ADD_PROPERTY(PropertyInfo(Variant::COLOR, "color"), "set_color", "get_color"); // TODO Might become obsolete ADD_PROPERTY(PropertyInfo(Variant::BOOL, "transparent", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_STORAGE), "set_transparent", "is_transparent"); ADD_PROPERTY(PropertyInfo(Variant::INT, "transparency_index"), "set_transparency_index", "get_transparency_index"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "random_tickable"), "set_random_tickable", "is_random_tickable"); ADD_PROPERTY(PropertyInfo(Variant::INT, "material_id"), "set_material_id", "get_material_id"); ADD_PROPERTY(PropertyInfo(Variant::INT, "geometry_type", PROPERTY_HINT_ENUM, "None,Cube,CustomMesh"), "set_geometry_type", "get_geometry_type"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "custom_mesh", PROPERTY_HINT_RESOURCE_TYPE, "Mesh"), "set_custom_mesh", "get_custom_mesh"); ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "collision_aabbs", PROPERTY_HINT_TYPE_STRING, itos(Variant::AABB) + ":"), "set_collision_aabbs", "get_collision_aabbs"); ADD_PROPERTY(PropertyInfo(Variant::INT, "collision_mask", PROPERTY_HINT_LAYERS_3D_PHYSICS), "set_collision_mask", "get_collision_mask"); BIND_ENUM_CONSTANT(GEOMETRY_NONE); BIND_ENUM_CONSTANT(GEOMETRY_CUBE); BIND_ENUM_CONSTANT(GEOMETRY_CUSTOM_MESH); BIND_ENUM_CONSTANT(GEOMETRY_MAX); BIND_ENUM_CONSTANT(SIDE_NEGATIVE_X); BIND_ENUM_CONSTANT(SIDE_POSITIVE_X); BIND_ENUM_CONSTANT(SIDE_NEGATIVE_Y); BIND_ENUM_CONSTANT(SIDE_POSITIVE_Y); BIND_ENUM_CONSTANT(SIDE_NEGATIVE_Z); BIND_ENUM_CONSTANT(SIDE_POSITIVE_Z); BIND_ENUM_CONSTANT(SIDE_COUNT); } Array Voxel::_b_get_collision_aabbs() const { Array array; array.resize(_collision_aabbs.size()); for (size_t i = 0; i < _collision_aabbs.size(); ++i) { array[i] = _collision_aabbs[i]; } return array; } void Voxel::_b_set_collision_aabbs(Array array) { for (int i = 0; i < array.size(); ++i) { const Variant v = array[i]; ERR_FAIL_COND(v.get_type() != Variant::AABB); } _collision_aabbs.resize(array.size()); for (int i = 0; i < array.size(); ++i) { const AABB aabb = array[i]; _collision_aabbs[i] = aabb; } }