diff --git a/Terrain3D.vcxproj b/Terrain3D.vcxproj index ed9989a3..18f927f5 100644 --- a/Terrain3D.vcxproj +++ b/Terrain3D.vcxproj @@ -149,6 +149,7 @@ + @@ -164,6 +165,7 @@ + diff --git a/Terrain3D.vcxproj.filters b/Terrain3D.vcxproj.filters index ac8c334c..95927250 100644 --- a/Terrain3D.vcxproj.filters +++ b/Terrain3D.vcxproj.filters @@ -69,6 +69,9 @@ 4. Headers + + 4. Headers + @@ -107,6 +110,9 @@ 5. C++ + + 5. C++ + diff --git a/project/addons/terrain_3d/editor.gd b/project/addons/terrain_3d/editor.gd index 13b9805d..f34c8f04 100644 --- a/project/addons/terrain_3d/editor.gd +++ b/project/addons/terrain_3d/editor.gd @@ -103,11 +103,11 @@ func _edit(p_object: Object) -> void: ui.set_visible(true) terrain.set_meta("_edit_lock_", true) - # Get alerted when a new asset list is loaded + # Get alerted when a new asset list is loaded if not terrain.assets_changed.is_connected(asset_dock.update_assets): terrain.assets_changed.connect(asset_dock.update_assets) asset_dock.update_assets() - # Get alerted when the region map changes + # Get alerted when the region map changes if not terrain.get_storage().region_map_changed.is_connected(update_region_grid): terrain.get_storage().region_map_changed.connect(update_region_grid) update_region_grid() @@ -180,9 +180,8 @@ func _forward_3d_gui_input(p_viewport_camera: Camera3D, p_event: InputEvent) -> ui.update_decal() ## Update region highlight - var region_size = terrain.get_storage().get_region_size() var region_position: Vector2 = ( Vector2(mouse_global_position.x, mouse_global_position.z) \ - / (region_size * terrain.get_mesh_vertex_spacing()) ).floor() + / (terrain.get_region_size() * terrain.get_mesh_vertex_spacing()) ).floor() if current_region_position != region_position: current_region_position = region_position update_region_grid() @@ -213,7 +212,7 @@ func _forward_3d_gui_input(p_viewport_camera: Camera3D, p_event: InputEvent) -> # If adjusting regions if editor.get_tool() == Terrain3DEditor.REGION: # Skip regions that already exist or don't - var has_region: bool = terrain.get_storage().has_region(mouse_global_position) + var has_region: bool = terrain.get_storage().has_regionp(mouse_global_position) var op: int = editor.get_operation() if ( has_region and op == Terrain3DEditor.ADD) or \ ( not has_region and op == Terrain3DEditor.SUBTRACT ): @@ -246,7 +245,7 @@ func update_region_grid() -> void: region_gizmo.show_rect = editor.get_tool() == Terrain3DEditor.REGION region_gizmo.use_secondary_color = editor.get_operation() == Terrain3DEditor.SUBTRACT region_gizmo.region_position = current_region_position - region_gizmo.region_size = terrain.get_storage().get_region_size() * terrain.get_mesh_vertex_spacing() + region_gizmo.region_size = terrain.get_region_size() * terrain.get_mesh_vertex_spacing() region_gizmo.grid = terrain.get_storage().get_region_locations() terrain.update_gizmos() diff --git a/project/addons/terrain_3d/src/ui.gd b/project/addons/terrain_3d/src/ui.gd index b8b3fa89..73a06c7c 100644 --- a/project/addons/terrain_3d/src/ui.gd +++ b/project/addons/terrain_3d/src/ui.gd @@ -412,9 +412,9 @@ func pick(p_global_position: Vector3) -> void: var color: Color match picking: Terrain3DEditor.HEIGHT: - color = plugin.terrain.get_storage().get_pixel(Terrain3DStorage.TYPE_HEIGHT, p_global_position) + color = plugin.terrain.get_storage().get_pixel(Terrain3DRegion.TYPE_HEIGHT, p_global_position) Terrain3DEditor.ROUGHNESS: - color = plugin.terrain.get_storage().get_pixel(Terrain3DStorage.TYPE_COLOR, p_global_position) + color = plugin.terrain.get_storage().get_pixel(Terrain3DRegion.TYPE_COLOR, p_global_position) Terrain3DEditor.COLOR: color = plugin.terrain.get_storage().get_color(p_global_position) Terrain3DEditor.ANGLE: diff --git a/project/addons/terrain_3d/tools/importer.gd b/project/addons/terrain_3d/tools/importer.gd index 8b5aba8e..1309ea90 100644 --- a/project/addons/terrain_3d/tools/importer.gd +++ b/project/addons/terrain_3d/tools/importer.gd @@ -61,19 +61,19 @@ func start_import(p_value: bool) -> void: storage = Terrain3DStorage.new() var imported_images: Array[Image] - imported_images.resize(Terrain3DStorage.TYPE_MAX) + imported_images.resize(Terrain3DRegion.TYPE_MAX) var min_max := Vector2(0, 1) var img: Image if height_file_name: img = Terrain3DUtil.load_image(height_file_name, ResourceLoader.CACHE_MODE_IGNORE, r16_range, r16_size) min_max = Terrain3DUtil.get_min_max(img) - imported_images[Terrain3DStorage.TYPE_HEIGHT] = img + imported_images[Terrain3DRegion.TYPE_HEIGHT] = img if control_file_name: img = Terrain3DUtil.load_image(control_file_name, ResourceLoader.CACHE_MODE_IGNORE) - imported_images[Terrain3DStorage.TYPE_CONTROL] = img + imported_images[Terrain3DRegion.TYPE_CONTROL] = img if color_file_name: img = Terrain3DUtil.load_image(color_file_name, ResourceLoader.CACHE_MODE_IGNORE) - imported_images[Terrain3DStorage.TYPE_COLOR] = img + imported_images[Terrain3DRegion.TYPE_COLOR] = img if assets.get_texture_count() == 0: material.show_checkered = false material.show_colormap = true diff --git a/src/constants.h b/src/constants.h index ab9e97eb..695fa6f2 100644 --- a/src/constants.h +++ b/src/constants.h @@ -18,12 +18,18 @@ using namespace godot; #define COLOR_NORMAL Color(0.5f, 0.5f, 1.0f, 1.0f) #define COLOR_CONTROL Color(as_float(enc_auto(true)), 0.f, 0.f, 1.0f) -// For consistency between msvc, gcc, clang - -#ifndef __FLT_MAX__ -#define __FLT_MAX__ FLT_MAX +#ifndef FLT_MAX +// For consistency between MSVC, gcc, clang +#define FLT_MAX __FLT_MAX__ #endif +#define V2_ZERO Vector2(0.f, 0.f) +#define V2_MAX Vector2(FLT_MAX, FLT_MAX) +#define V3_ZERO Vector3(0.f, 0.f, 0.f) +#define V3_MAX Vector3(FLT_MAX, FLT_MAX, FLT_MAX) +#define V2I_ZERO Vector2i(0, 0) +#define V2I_MAX Vector2i(INT32_MAX, INT32_MAX) + // Set class name for logger.h #define CLASS_NAME() const String __class__ = get_class_static() + \ @@ -68,15 +74,15 @@ using namespace godot; return ret; \ } -#define IS_STORAGE_INIT(ret) \ - if (_terrain == nullptr || _terrain->get_storage().is_null()) { \ - return ret; \ +#define IS_STORAGE_INIT(ret) \ + if (_terrain == nullptr || _terrain->get_storage() == nullptr) { \ + return ret; \ } -#define IS_STORAGE_INIT_MESG(mesg, ret) \ - if (_terrain == nullptr || _terrain->get_storage().is_null()) { \ - LOG(ERROR, mesg); \ - return ret; \ +#define IS_STORAGE_INIT_MESG(mesg, ret) \ + if (_terrain == nullptr || _terrain->get_storage() == nullptr) { \ + LOG(ERROR, mesg); \ + return ret; \ } #endif // CONSTANTS_CLASS_H \ No newline at end of file diff --git a/src/geoclipmap.cpp b/src/geoclipmap.cpp index 5d4bdcce..ef6de753 100644 --- a/src/geoclipmap.cpp +++ b/src/geoclipmap.cpp @@ -95,7 +95,7 @@ Vector GeoClipMap::generate(const int p_size, const int p_levels) { } } - aabb = AABB(Vector3(0.f, 0.f, 0.f), Vector3(PATCH_VERT_RESOLUTION, 0.1f, PATCH_VERT_RESOLUTION)); + aabb = AABB(V3_ZERO, Vector3(PATCH_VERT_RESOLUTION, 0.1f, PATCH_VERT_RESOLUTION)); tile_mesh = _create_mesh(vertices, indices, aabb); } diff --git a/src/register_types.cpp b/src/register_types.cpp index e86c70b7..a86addca 100644 --- a/src/register_types.cpp +++ b/src/register_types.cpp @@ -20,6 +20,7 @@ void initialize_terrain_3d(ModuleInitializationLevel p_level) { ClassDB::register_class(); ClassDB::register_class(); ClassDB::register_class(); + ClassDB::register_class(); ClassDB::register_class(); ClassDB::register_class(); ClassDB::register_class(); // Deprecated 0.9.2 - Remove 0.9.3+ diff --git a/src/terrain_3d.cpp b/src/terrain_3d.cpp index 8f44c267..90e74b37 100644 --- a/src/terrain_3d.cpp +++ b/src/terrain_3d.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -29,17 +30,16 @@ int Terrain3D::debug_level{ ERROR }; void Terrain3D::_initialize() { - LOG(INFO, "Checking instancer, material, storage, assets, signal, and mesh initialization"); + LOG(INFO, "Checking initialization of main subsystems"); // Make blank objects if needed if (_material.is_null()) { LOG(DEBUG, "Creating blank material"); _material.instantiate(); } - if (_storage.is_null()) { + if (_storage == nullptr) { LOG(DEBUG, "Creating blank storage"); - _storage.instantiate(); - _storage->set_version(Terrain3DStorage::CURRENT_VERSION); + _storage = memnew(Terrain3DStorage); } if (_assets.is_null()) { LOG(DEBUG, "Creating blank texture list"); @@ -51,40 +51,35 @@ void Terrain3D::_initialize() { } // Connect signals - // Region size changed, update material - if (!_storage->is_connected("region_size_changed", callable_mp(_material.ptr(), &Terrain3DMaterial::_update_maps))) { - LOG(DEBUG, "Connecting region_size_changed signal to _material->_update_regions()"); - _storage->connect("region_size_changed", callable_mp(_material.ptr(), &Terrain3DMaterial::_update_maps)); + // Any region was changed, update region labels + if (!_storage->is_connected("region_map_changed", callable_mp(this, &Terrain3D::update_region_labels))) { + LOG(DEBUG, "Connecting _storage::region_map_changed signal to set_show_region_locations()"); + _storage->connect("region_map_changed", callable_mp(this, &Terrain3D::update_region_labels)); } // Any map was regenerated or regions changed, update material if (!_storage->is_connected("maps_changed", callable_mp(_material.ptr(), &Terrain3DMaterial::_update_maps))) { LOG(DEBUG, "Connecting _storage::maps_changed signal to _material->_update_maps()"); _storage->connect("maps_changed", callable_mp(_material.ptr(), &Terrain3DMaterial::_update_maps)); } - // Height map was regenerated - update aabbs - Probably remove and just use maps_edited + // Height map was regenerated, update aabbs if (!_storage->is_connected("height_maps_changed", callable_mp(this, &Terrain3D::update_aabbs))) { LOG(DEBUG, "Connecting _storage::height_maps_changed signal to update_aabbs()"); _storage->connect("height_maps_changed", callable_mp(this, &Terrain3D::update_aabbs)); } + // Connect height changes to update instances + if (!_storage->is_connected("maps_edited", callable_mp(_instancer, &Terrain3DInstancer::update_transforms))) { + LOG(DEBUG, "Connecting maps_edited signal to update_transforms()"); + _storage->connect("maps_edited", callable_mp(_instancer, &Terrain3DInstancer::update_transforms)); + } // Texture assets changed, update material if (!_assets->is_connected("textures_changed", callable_mp(_material.ptr(), &Terrain3DMaterial::_update_texture_arrays))) { LOG(DEBUG, "Connecting _assets.textures_changed to _material->_update_texture_arrays()"); _assets->connect("textures_changed", callable_mp(_material.ptr(), &Terrain3DMaterial::_update_texture_arrays)); } // MeshAssets changed, update instancer - if (!_assets->is_connected("meshes_changed", callable_mp(_instancer, &Terrain3DInstancer::_update_mmis).bind(Vector2i(INT32_MAX, INT32_MAX), -1))) { + if (!_assets->is_connected("meshes_changed", callable_mp(_instancer, &Terrain3DInstancer::_update_mmis).bind(V2I_MAX, -1))) { LOG(DEBUG, "Connecting _assets.meshes_changed to _instancer->_update_mmis()"); - _assets->connect("meshes_changed", callable_mp(_instancer, &Terrain3DInstancer::_update_mmis).bind(Vector2i(INT32_MAX, INT32_MAX), -1)); - } - // New multimesh added to storage, rebuild instancer - if (!_storage->is_connected("multimeshes_changed", callable_mp(_instancer, &Terrain3DInstancer::_rebuild_mmis))) { - LOG(DEBUG, "Connecting _storage::multimeshes_changed signal to _rebuild_mmis()"); - _storage->connect("multimeshes_changed", callable_mp(_instancer, &Terrain3DInstancer::_rebuild_mmis)); - } - // Connect height changes to update instances - if (!_storage->is_connected("maps_edited", callable_mp(_instancer, &Terrain3DInstancer::update_transforms))) { - LOG(DEBUG, "Connecting maps_edited signal to update_transforms()"); - _storage->connect("maps_edited", callable_mp(_instancer, &Terrain3DInstancer::update_transforms)); + _assets->connect("meshes_changed", callable_mp(_instancer, &Terrain3DInstancer::_update_mmis).bind(V2I_MAX, -1)); } // Initialize the system @@ -125,6 +120,29 @@ void Terrain3D::__process(const double p_delta) { } } +void Terrain3D::_build_containers() { + _label_nodes = memnew(Node); + _label_nodes->set_name("Labels"); + add_child(_label_nodes, true); + _mmi_nodes = memnew(Node); + _mmi_nodes->set_name("MMIs"); + add_child(_mmi_nodes, true); +} + +void Terrain3D::_destroy_containers() { + memdelete_safely(_label_nodes); + memdelete_safely(_mmi_nodes); +} + +void Terrain3D::_destroy_labels() { + Array labels = _label_nodes->get_children(); + LOG(DEBUG, "Destroying ", labels.size(), " region labels"); + for (int i = 0; i < labels.size(); i++) { + Node *label = cast_to(labels[i]); + memdelete_safely(label); + } +} + void Terrain3D::_setup_mouse_picking() { if (!is_inside_tree()) { LOG(ERROR, "Not inside the tree, skipping mouse setup"); @@ -185,7 +203,7 @@ void Terrain3D::_destroy_mouse_picking() { memdelete_safely(_mouse_quad); LOG(DEBUG, "Freeing mouse_cam"); memdelete_safely(_mouse_cam); - LOG(DEBUG, "memdelete mouse_vp"); + LOG(DEBUG, "Freeing mouse_vp"); memdelete_safely(_mouse_vp); } @@ -211,7 +229,7 @@ void Terrain3D::_grab_camera() { } void Terrain3D::_build_meshes(const int p_mesh_lods, const int p_mesh_size) { - if (!is_inside_tree() || !_storage.is_valid()) { + if (!is_inside_tree() || _storage == nullptr) { LOG(DEBUG, "Not inside the tree or no valid storage, skipping build"); return; } @@ -270,7 +288,7 @@ void Terrain3D::_build_meshes(const int p_mesh_lods, const int p_mesh_size) { update_aabbs(); // Force a snap update - _camera_last_position = Vector2(__FLT_MAX__, __FLT_MAX__); + _camera_last_position = V2_MAX; } /** @@ -357,7 +375,7 @@ void Terrain3D::_build_collision() { if (IS_EDITOR && !_show_debug_collision) { return; } - if (_storage.is_null()) { + if (_storage == nullptr) { LOG(ERROR, "Storage missing, cannot create collision"); return; } @@ -397,39 +415,45 @@ void Terrain3D::_update_collision() { } int time = Time::get_singleton()->get_ticks_msec(); - int region_size = _storage->get_region_size(); - int shape_size = region_size + 1; + int shape_size = _region_size + 1; float hole_const = NAN; // DEPRECATED - Jolt v0.12 supports NAN. Remove check when it's old. if (ProjectSettings::get_singleton()->get_setting("physics/3d/physics_engine") == "JoltPhysics3D") { - hole_const = __FLT_MAX__; + hole_const = FLT_MAX; } for (int i = 0; i < _storage->get_region_count(); i++) { PackedRealArray map_data = PackedRealArray(); map_data.resize(shape_size * shape_size); - Vector2i global_loc = Vector2i(_storage->get_region_locations()[i]) * region_size; + Vector2i region_loc = _storage->get_region_locations()[i]; + Vector2i global_loc = region_loc * _region_size; Vector3 global_pos = Vector3(global_loc.x, 0.f, global_loc.y); Ref map, map_x, map_z, map_xz; Ref cmap, cmap_x, cmap_z, cmap_xz; - map = _storage->get_map_region(TYPE_HEIGHT, i); - cmap = _storage->get_map_region(TYPE_CONTROL, i); - int region_id = _storage->get_region_id(Vector3(global_pos.x + region_size, 0.f, global_pos.z) * _mesh_vertex_spacing); - if (region_id >= 0) { - map_x = _storage->get_map_region(TYPE_HEIGHT, region_id); - cmap_x = _storage->get_map_region(TYPE_CONTROL, region_id); + Ref region = _storage->get_region(region_loc); + if (region.is_null()) { + LOG(ERROR, "Region ", region_loc, " not found"); + continue; + } + map = region->get_map(TYPE_HEIGHT); + cmap = region->get_map(TYPE_CONTROL); + + region = _storage->get_regionp(Vector3(global_pos.x + _region_size, 0.f, global_pos.z) * _mesh_vertex_spacing); + if (region.is_valid()) { + map_x = region->get_map(TYPE_HEIGHT); + cmap_x = region->get_map(TYPE_CONTROL); } - region_id = _storage->get_region_id(Vector3(global_pos.x, 0.f, global_pos.z + region_size) * _mesh_vertex_spacing); - if (region_id >= 0) { - map_z = _storage->get_map_region(TYPE_HEIGHT, region_id); - cmap_z = _storage->get_map_region(TYPE_CONTROL, region_id); + region = _storage->get_regionp(Vector3(global_pos.x, 0.f, global_pos.z + _region_size) * _mesh_vertex_spacing); + if (region.is_valid()) { + map_z = region->get_map(TYPE_HEIGHT); + cmap_z = region->get_map(TYPE_CONTROL); } - region_id = _storage->get_region_id(Vector3(global_pos.x + region_size, 0.f, global_pos.z + region_size) * _mesh_vertex_spacing); - if (region_id >= 0) { - map_xz = _storage->get_map_region(TYPE_HEIGHT, region_id); - cmap_xz = _storage->get_map_region(TYPE_CONTROL, region_id); + region = _storage->get_regionp(Vector3(global_pos.x + _region_size, 0.f, global_pos.z + _region_size) * _mesh_vertex_spacing); + if (region.is_valid()) { + map_xz = region->get_map(TYPE_HEIGHT); + cmap_xz = region->get_map(TYPE_CONTROL); } for (int z = 0; z < shape_size; z++) { @@ -442,21 +466,21 @@ void Terrain3D::_update_collision() { int index = shape_size - 1 - z + x * shape_size; // Set heights on local map, or adjacent maps if on the last row/col - if (x < region_size && z < region_size) { + if (x < _region_size && z < _region_size) { map_data[index] = (is_hole(cmap->get_pixel(x, z).r)) ? hole_const : map->get_pixel(x, z).r; - } else if (x == region_size && z < region_size) { + } else if (x == _region_size && z < _region_size) { if (map_x.is_valid()) { map_data[index] = (is_hole(cmap_x->get_pixel(0, z).r)) ? hole_const : map_x->get_pixel(0, z).r; } else { map_data[index] = 0.0f; } - } else if (z == region_size && x < region_size) { + } else if (z == _region_size && x < _region_size) { if (map_z.is_valid()) { map_data[index] = (is_hole(cmap_z->get_pixel(x, 0).r)) ? hole_const : map_z->get_pixel(x, 0).r; } else { map_data[index] = 0.0f; } - } else if (x == region_size && z == region_size) { + } else if (x == _region_size && z == _region_size) { if (map_xz.is_valid()) { map_data[index] = (is_hole(cmap_xz->get_pixel(0, 0).r)) ? hole_const : map_xz->get_pixel(0, 0).r; } else { @@ -470,7 +494,7 @@ void Terrain3D::_update_collision() { //Transform3D xform = Transform3D(Basis(), global_pos); // Rotated shape Y=90 for -90 rotated array index Transform3D xform = Transform3D(Basis(Vector3(0.f, 1.f, 0.f), Math_PI * .5f), - global_pos + Vector3(region_size, 0.f, region_size) * .5f); + global_pos + Vector3(_region_size, 0.f, _region_size) * .5f); xform.scale(Vector3(_mesh_vertex_spacing, 1.f, _mesh_vertex_spacing)); if (!_show_debug_collision) { @@ -541,11 +565,11 @@ void Terrain3D::_destroy_instancer() { void Terrain3D::_generate_triangles(PackedVector3Array &p_vertices, PackedVector2Array *p_uvs, const int32_t p_lod, const Terrain3DStorage::HeightFilter p_filter, const bool p_require_nav, const AABB &p_global_aabb) const { - ERR_FAIL_COND(!_storage.is_valid()); + ERR_FAIL_COND(_storage == nullptr); int32_t step = 1 << CLAMP(p_lod, 0, 8); if (!p_global_aabb.has_volume()) { - int32_t region_size = (int)_storage->get_region_size(); + int32_t region_size = (int32_t)_region_size; TypedArray region_locations = _storage->get_region_locations(); for (int r = 0; r < region_locations.size(); ++r) { @@ -654,6 +678,22 @@ void Terrain3D::set_debug_level(const int p_level) { debug_level = CLAMP(p_level, 0, DEBUG_MAX); } +void Terrain3D::set_region_size(const RegionSize p_size) { + LOG(INFO, p_size); + //ERR_FAIL_COND(p_size < SIZE_64); + //ERR_FAIL_COND(p_size > SIZE_2048); + ERR_FAIL_COND(p_size != SIZE_1024); + _region_size = p_size; + // Region size changed, update downstream + if (_storage) { + _storage->_region_size = _region_size; + _storage->_region_sizev = Vector2i(_region_size, _region_size); + } + if (_material.is_valid()) { + _material->_update_maps(); + } +} + void Terrain3D::set_mesh_lods(const int p_count) { if (_mesh_lods != p_count) { _clear_meshes(); @@ -683,32 +723,44 @@ void Terrain3D::set_mesh_vertex_spacing(const real_t p_spacing) { _destroy_collision(); _destroy_instancer(); _initialize(); + _storage->_mesh_vertex_spacing = spacing; } if (IS_EDITOR && _plugin != nullptr) { _plugin->call("update_region_grid"); } } -void Terrain3D::set_material(const Ref &p_material) { - if (_material != p_material) { +void Terrain3D::set_storage_directory(String p_dir) { + LOG(INFO, "Setting storage directory to ", p_dir); + if (_storage_directory != p_dir) { _clear_meshes(); - LOG(INFO, "Setting material"); - _material = p_material; + _destroy_collision(); + _destroy_instancer(); + memdelete_safely(_storage); + _storage_directory = p_dir; _initialize(); - emit_signal("material_changed"); } } -// This is run after the object has loaded and initialized -void Terrain3D::set_storage(const Ref &p_storage) { - if (_storage != p_storage) { +String Terrain3D::get_storage_directory() const { + if (_storage == nullptr) { + return ""; + } + return _storage_directory; +} + +void Terrain3D::set_save_16_bit(const bool p_enabled) { + LOG(INFO, p_enabled); + _save_16_bit = p_enabled; +} + +void Terrain3D::set_material(const Ref &p_material) { + if (_material != p_material) { _clear_meshes(); - _destroy_collision(); - _destroy_instancer(); - LOG(INFO, "Setting storage"); - _storage = p_storage; + LOG(INFO, "Setting material"); + _material = p_material; _initialize(); - emit_signal("storage_changed"); + emit_signal("material_changed"); } } @@ -802,7 +854,7 @@ void Terrain3D::set_show_debug_collision(const bool p_enabled) { LOG(INFO, "Setting show collision: ", p_enabled); _show_debug_collision = p_enabled; _destroy_collision(); - if (_storage.is_valid() && _show_debug_collision) { + if (_storage != nullptr && _show_debug_collision) { _build_collision(); } } @@ -941,7 +993,7 @@ void Terrain3D::snap(const Vector3 &p_cam_pos) { } void Terrain3D::update_aabbs() { - if (_meshes.is_empty() || _storage.is_null()) { + if (_meshes.is_empty() || _storage == nullptr) { LOG(DEBUG, "Update AABB called before terrain meshes built. Returning."); return; } @@ -984,11 +1036,10 @@ void Terrain3D::update_aabbs() { } } -/* Iterate over ground to find intersection point between two rays: +/* Returns the point a ray intersects the ground using the GPU depth texture * p_src_pos (camera position) * p_direction (camera direction looking at the terrain) - * test_dir (camera direction 0 Y, traversing terrain along height - * Returns vec3(Double max 3.402823466e+38F) on no intersection. Test w/ if (var.x < 3.4e38) + * Returns Vec3(NAN) on error or vec3(3.402823466e+38F) on no intersection. Test w/ if (var.x < 3.4e38) */ Vector3 Terrain3D::get_intersection(const Vector3 &p_src_pos, const Vector3 &p_direction) { if (!is_instance_valid(_camera_instance_id)) { @@ -1029,7 +1080,7 @@ Vector3 Terrain3D::get_intersection(const Vector3 &p_src_pos, const Vector3 &p_d Vector2 screen_rg = Vector2(screen_depth.r, screen_depth.g); real_t normalized_distance = screen_rg.dot(Vector2(1.f, 1.f / 255.f)); if (normalized_distance < 0.00001f) { - return Vector3(__FLT_MAX__, __FLT_MAX__, __FLT_MAX__); + return V3_MAX; } // Necessary for a correct value depth = 1 if (normalized_distance > 0.9999f) { @@ -1044,6 +1095,44 @@ Vector3 Terrain3D::get_intersection(const Vector3 &p_src_pos, const Vector3 &p_d return point; } +void Terrain3D::set_show_region_labels(const bool p_enabled) { + LOG(INFO, "Setting show region labels: ", p_enabled); + if (_show_region_labels != p_enabled) { + _show_region_labels = p_enabled; + update_region_labels(); + } +} + +void Terrain3D::update_region_labels() { + _destroy_labels(); + if (_show_region_labels && _storage != nullptr) { + Array region_locations = _storage->get_region_locations(); + LOG(DEBUG, "Creating ", region_locations.size(), " region labels"); + for (int i = 0; i < region_locations.size(); i++) { + Label3D *label = memnew(Label3D); + String text = region_locations[i]; + label->set_name("Label3D" + text.replace(" ", "")); + label->set_pixel_size(.001f); + label->set_billboard_mode(BaseMaterial3D::BILLBOARD_ENABLED); + label->set_draw_flag(Label3D::FLAG_DOUBLE_SIDED, true); + label->set_draw_flag(Label3D::FLAG_DISABLE_DEPTH_TEST, true); + label->set_draw_flag(Label3D::FLAG_FIXED_SIZE, true); + label->set_text(text); + label->set_modulate(Color(1.f, 1.f, 1.f, .5f)); + label->set_outline_modulate(Color(0.f, 0.f, 0.f, .5f)); + label->set_font_size(64); + label->set_outline_size(10); + label->set_visibility_range_end(3072.f * _mesh_vertex_spacing); + label->set_visibility_range_end_margin(256.f); + label->set_visibility_range_fade_mode(GeometryInstance3D::VISIBILITY_RANGE_FADE_SELF); + _label_nodes->add_child(label, true); + Vector2i loc = region_locations[i]; + Vector3 pos = Vector3(real_t(loc.x) + .5f, 0.f, real_t(loc.y) + .5f) * _region_size * _mesh_vertex_spacing; + label->set_position(pos); + } + } +} + /** * Generates a static ArrayMesh for the terrain. * p_lod (0-8): Determines the granularity of the generated mesh. @@ -1056,7 +1145,7 @@ Vector3 Terrain3D::get_intersection(const Vector3 &p_src_pos, const Vector3 &p_d Ref Terrain3D::bake_mesh(const int p_lod, const Terrain3DStorage::HeightFilter p_filter) const { LOG(INFO, "Baking mesh at lod: ", p_lod, " with filter: ", p_filter); Ref result; - ERR_FAIL_COND_V(!_storage.is_valid(), result); + ERR_FAIL_COND_V(_storage == nullptr, result); Ref st; st.instantiate(); @@ -1098,11 +1187,8 @@ PackedVector3Array Terrain3D::generate_nav_mesh_source_geometry(const AABB &p_gl PackedStringArray Terrain3D::_get_configuration_warnings() const { PackedStringArray psa; - if (_storage.is_valid()) { - String ext = _storage->get_path().get_extension(); - if (ext != "res") { - psa.push_back("Storage resource is not saved as a binary resource file. Click the arrow to the right of `Storage`, then `Save As...` a `*.res` file."); - } + if (_storage_directory.is_empty()) { + psa.push_back("No storage directory specified. Select a directory then save the scene to write data."); } if (!psa.is_empty()) { psa.push_back("To update this message, deselect and reselect Terrain3D in the Scene panel."); @@ -1137,6 +1223,7 @@ void Terrain3D::_notification(const int p_what) { case NOTIFICATION_POSTINITIALIZE: { // Object initialized, before script is attached LOG(INFO, "NOTIFICATION_POSTINITIALIZE"); + _build_containers(); break; } @@ -1157,7 +1244,7 @@ void Terrain3D::_notification(const int p_what) { set_notify_transform(true); set_meta("_edit_lock_", true); _setup_mouse_picking(); - _initialize(); + _initialize(); // Rebuild anything freed: meshes, collision, instancer set_process(true); break; } @@ -1200,10 +1287,10 @@ void Terrain3D::_notification(const int p_what) { case NOTIFICATION_EDITOR_PRE_SAVE: { // Editor Node is about to the current scene LOG(INFO, "NOTIFICATION_EDITOR_PRE_SAVE"); - if (!_storage.is_valid()) { + if (_storage == nullptr) { LOG(DEBUG, "Save requested, but no valid storage. Skipping"); } else { - _storage->save(); + _storage->save_directory(_storage_directory); } if (!_material.is_valid()) { LOG(DEBUG, "Save requested, but no valid material. Skipping"); @@ -1238,9 +1325,7 @@ void Terrain3D::_notification(const int p_what) { LOG(INFO, "NOTIFICATION_EXIT_TREE"); set_process(false); _clear_meshes(); - _destroy_collision(); _destroy_mouse_picking(); - _destroy_instancer(); break; } @@ -1255,6 +1340,11 @@ void Terrain3D::_notification(const int p_what) { case NOTIFICATION_PREDELETE: { // Object is about to be deleted LOG(INFO, "NOTIFICATION_PREDELETE"); + _destroy_collision(); + _destroy_instancer(); + _destroy_labels(); + _destroy_containers(); + memdelete_safely(_storage); break; } @@ -1264,9 +1354,28 @@ void Terrain3D::_notification(const int p_what) { } void Terrain3D::_bind_methods() { + //BIND_ENUM_CONSTANT(SIZE_64); + //BIND_ENUM_CONSTANT(SIZE_128); + //BIND_ENUM_CONSTANT(SIZE_256); + //BIND_ENUM_CONSTANT(SIZE_512); + BIND_ENUM_CONSTANT(SIZE_1024); + //BIND_ENUM_CONSTANT(SIZE_2048); + ClassDB::bind_method(D_METHOD("get_version"), &Terrain3D::get_version); ClassDB::bind_method(D_METHOD("set_debug_level", "level"), &Terrain3D::set_debug_level); ClassDB::bind_method(D_METHOD("get_debug_level"), &Terrain3D::get_debug_level); + ClassDB::bind_method(D_METHOD("set_show_region_labels", "enabled"), &Terrain3D::set_show_region_labels); + ClassDB::bind_method(D_METHOD("get_show_region_labels"), &Terrain3D::get_show_region_labels); + + ClassDB::bind_method(D_METHOD("get_storage"), &Terrain3D::get_storage); + ClassDB::bind_method(D_METHOD("set_storage_directory", "directory"), &Terrain3D::set_storage_directory); + ClassDB::bind_method(D_METHOD("get_storage_directory"), &Terrain3D::get_storage_directory); + ClassDB::bind_method(D_METHOD("set_save_16_bit", "enabled"), &Terrain3D::set_save_16_bit); + ClassDB::bind_method(D_METHOD("get_save_16_bit"), &Terrain3D::get_save_16_bit); + + ClassDB::bind_method(D_METHOD("set_region_size", "size"), &Terrain3D::set_region_size); + ClassDB::bind_method(D_METHOD("get_region_size"), &Terrain3D::get_region_size); + ClassDB::bind_method(D_METHOD("set_mesh_lods", "count"), &Terrain3D::set_mesh_lods); ClassDB::bind_method(D_METHOD("get_mesh_lods"), &Terrain3D::get_mesh_lods); ClassDB::bind_method(D_METHOD("set_mesh_size", "size"), &Terrain3D::set_mesh_size); @@ -1276,8 +1385,6 @@ void Terrain3D::_bind_methods() { ClassDB::bind_method(D_METHOD("set_material", "material"), &Terrain3D::set_material); ClassDB::bind_method(D_METHOD("get_material"), &Terrain3D::get_material); - ClassDB::bind_method(D_METHOD("set_storage", "storage"), &Terrain3D::set_storage); - ClassDB::bind_method(D_METHOD("get_storage"), &Terrain3D::get_storage); ClassDB::bind_method(D_METHOD("set_assets", "assets"), &Terrain3D::set_assets); ClassDB::bind_method(D_METHOD("get_assets"), &Terrain3D::get_assets); ClassDB::bind_method(D_METHOD("get_instancer"), &Terrain3D::get_instancer); @@ -1314,10 +1421,16 @@ void Terrain3D::_bind_methods() { ClassDB::bind_method(D_METHOD("bake_mesh", "lod", "filter"), &Terrain3D::bake_mesh); ClassDB::bind_method(D_METHOD("generate_nav_mesh_source_geometry", "global_aabb", "require_nav"), &Terrain3D::generate_nav_mesh_source_geometry, DEFVAL(true)); + int ro_flags = PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_READ_ONLY; + ADD_PROPERTY(PropertyInfo(Variant::STRING, "version", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_READ_ONLY), "", "get_version"); - ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "storage", PROPERTY_HINT_RESOURCE_TYPE, "Terrain3DStorage"), "set_storage", "get_storage"); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "storage_directory", PROPERTY_HINT_DIR), "set_storage_directory", "get_storage_directory"); + //ADD_PROPERTY(PropertyInfo(Variant::INT, "region_size", PROPERTY_HINT_ENUM, "64:64, 128:128, 256:256, 512:512, 1024:1024, 2048:2048"), "set_region_size", "get_region_size"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "region_size", PROPERTY_HINT_ENUM, "1024:1024"), "set_region_size", "get_region_size"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "save_16_bit", PROPERTY_HINT_NONE), "set_save_16_bit", "get_save_16_bit"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "material", PROPERTY_HINT_RESOURCE_TYPE, "Terrain3DMaterial"), "set_material", "get_material"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "assets", PROPERTY_HINT_RESOURCE_TYPE, "Terrain3DAssets"), "set_assets", "get_assets"); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "storage", PROPERTY_HINT_NONE, "Terrain3DStorage", PROPERTY_USAGE_NONE), "", "get_storage"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "instancer", PROPERTY_HINT_NONE, "Terrain3DInstancer", PROPERTY_USAGE_NONE), "", "get_instancer"); ADD_GROUP("Renderer", "render_"); @@ -1340,9 +1453,9 @@ void Terrain3D::_bind_methods() { ADD_GROUP("Debug", "debug_"); ADD_PROPERTY(PropertyInfo(Variant::INT, "debug_level", PROPERTY_HINT_ENUM, "Errors,Info,Debug,Debug Continuous"), "set_debug_level", "get_debug_level"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "debug_show_collision"), "set_show_debug_collision", "get_show_debug_collision"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "debug_show_region_labels"), "set_show_region_labels", "get_show_region_labels"); ADD_SIGNAL(MethodInfo("material_changed")); - ADD_SIGNAL(MethodInfo("storage_changed")); ADD_SIGNAL(MethodInfo("assets_changed")); // DEPRECATED 0.9.2 - Remove 0.9.3+ diff --git a/src/terrain_3d.h b/src/terrain_3d.h index 12f21504..5db4c47b 100644 --- a/src/terrain_3d.h +++ b/src/terrain_3d.h @@ -24,22 +24,41 @@ class Terrain3D : public Node3D { GDCLASS(Terrain3D, Node3D); CLASS_NAME(); +public: // Constants + enum RegionSize { + //SIZE_64 = 64, + //SIZE_128 = 128, + //SIZE_256 = 256, + //SIZE_512 = 512, + SIZE_1024 = 1024, + //SIZE_2048 = 2048, + }; + +private: // Terrain state String _version = "0.9.3-dev"; bool _is_inside_world = false; bool _initialized = false; // Terrain settings + RegionSize _region_size = SIZE_1024; int _mesh_size = 48; int _mesh_lods = 7; real_t _mesh_vertex_spacing = 1.0f; + String _storage_directory; + bool _save_16_bit = false; + bool _show_region_labels = false; + Terrain3DStorage *_storage = nullptr; Ref _material; - Ref _storage; Ref _assets; Terrain3DInstancer *_instancer = nullptr; Terrain3DEditor *_editor = nullptr; + // Parent containers for child nodes + Node *_label_nodes; + Node *_mmi_nodes; + // Editor components EditorPlugin *_plugin = nullptr; // Current editor or gameplay camera we are centering the terrain on. @@ -47,7 +66,7 @@ class Terrain3D : public Node3D { uint64_t _camera_instance_id = 0; // X,Z Position of the camera during the previous snapping. Set to max real_t value to force a snap update. - Vector2 _camera_last_position = Vector2(__FLT_MAX__, __FLT_MAX__); + Vector2 _camera_last_position = V2_MAX; // Meshes and Mesh instances Vector _meshes; @@ -82,6 +101,10 @@ class Terrain3D : public Node3D { void _initialize(); void __process(const double p_delta); + void _build_containers(); + void _destroy_containers(); + void _destroy_labels(); + void _setup_mouse_picking(); void _destroy_mouse_picking(); void _grab_camera(); @@ -111,6 +134,8 @@ class Terrain3D : public Node3D { String get_version() const { return _version; } void set_debug_level(const int p_level); int get_debug_level() const { return debug_level; } + void set_region_size(const RegionSize p_size); + RegionSize get_region_size() const { return _region_size; } void set_mesh_lods(const int p_count); int get_mesh_lods() const { return _mesh_lods; } void set_mesh_size(const int p_size); @@ -118,15 +143,20 @@ class Terrain3D : public Node3D { void set_mesh_vertex_spacing(const real_t p_spacing); real_t get_mesh_vertex_spacing() const { return _mesh_vertex_spacing; } + Terrain3DStorage *get_storage() const { return _storage; } + void set_storage_directory(String p_dir); + String get_storage_directory() const; + void set_save_16_bit(const bool p_enabled); + bool get_save_16_bit() const { return _save_16_bit; } + void set_material(const Ref &p_material); Ref get_material() const { return _material; } - void set_storage(const Ref &p_storage); - Ref get_storage() const { return _storage; } void set_assets(const Ref &p_assets); Ref get_assets() const { return _assets; } // Instancer Terrain3DInstancer *get_instancer() const { return _instancer; } + Node *get_mmi_parent() const { return _mmi_nodes; } // Editor components void set_editor(Terrain3DEditor *p_editor); @@ -164,11 +194,15 @@ class Terrain3D : public Node3D { void update_aabbs(); Vector3 get_intersection(const Vector3 &p_src_pos, const Vector3 &p_direction); + void set_show_region_labels(const bool p_enabled); + bool get_show_region_labels() const { return _show_region_labels; } + void update_region_labels(); + // Baking methods Ref bake_mesh(const int p_lod, const Terrain3DStorage::HeightFilter p_filter = Terrain3DStorage::HEIGHT_FILTER_NEAREST) const; PackedVector3Array generate_nav_mesh_source_geometry(const AABB &p_global_aabb, const bool p_require_nav = true) const; - // Misc + // Godot Callbacks PackedStringArray _get_configuration_warnings() const override; // DEPRECATED 0.9.2 - Remove 0.9.3+ @@ -180,4 +214,6 @@ class Terrain3D : public Node3D { static void _bind_methods(); }; +VARIANT_ENUM_CAST(Terrain3D::RegionSize); + #endif // TERRAIN3D_CLASS_H diff --git a/src/terrain_3d_asset_resource.h b/src/terrain_3d_asset_resource.h index 1376335d..681ac2f5 100644 --- a/src/terrain_3d_asset_resource.h +++ b/src/terrain_3d_asset_resource.h @@ -15,8 +15,8 @@ class Terrain3DAssetResource : public Resource { friend class Terrain3DAssets; public: - Terrain3DAssetResource(){}; - ~Terrain3DAssetResource(){}; + Terrain3DAssetResource() {} + ~Terrain3DAssetResource() {} virtual void clear() = 0; virtual void set_name(const String &p_name) = 0; diff --git a/src/terrain_3d_assets.cpp b/src/terrain_3d_assets.cpp index fbd380d7..d7592c55 100644 --- a/src/terrain_3d_assets.cpp +++ b/src/terrain_3d_assets.cpp @@ -166,8 +166,8 @@ void Terrain3DAssets::_update_texture_files() { // Detect image sizes and formats LOG(INFO, "Validating texture sizes"); - Vector2i albedo_size = Vector2i(0, 0); - Vector2i normal_size = Vector2i(0, 0); + Vector2i albedo_size = V2I_ZERO; + Vector2i normal_size = V2I_ZERO; Image::Format albedo_format = Image::FORMAT_MAX; Image::Format normal_format = Image::FORMAT_MAX; @@ -221,19 +221,19 @@ void Terrain3DAssets::_update_texture_files() { } } - if (normal_size == Vector2i(0, 0)) { + if (normal_size == V2I_ZERO) { normal_size = albedo_size; - } else if (albedo_size == Vector2i(0, 0)) { + } else if (albedo_size == V2I_ZERO) { albedo_size = normal_size; } - if (albedo_size == Vector2i(0, 0)) { + if (albedo_size == V2I_ZERO) { albedo_size = Vector2i(1024, 1024); normal_size = Vector2i(1024, 1024); } // Generate TextureArrays and replace nulls with a empty image - if (_generated_albedo_textures.is_dirty() && albedo_size != Vector2i(0, 0)) { + if (_generated_albedo_textures.is_dirty() && albedo_size != V2I_ZERO) { LOG(INFO, "Regenerating albedo texture array"); Array albedo_texture_array; for (int i = 0; i < _texture_list.size(); i++) { @@ -259,7 +259,7 @@ void Terrain3DAssets::_update_texture_files() { } } - if (_generated_normal_textures.is_dirty() && normal_size != Vector2i(0, 0)) { + if (_generated_normal_textures.is_dirty() && normal_size != V2I_ZERO) { LOG(INFO, "Regenerating normal texture arrays"); Array normal_texture_array; diff --git a/src/terrain_3d_editor.cpp b/src/terrain_3d_editor.cpp index 57268fb9..a86f2f2c 100644 --- a/src/terrain_3d_editor.cpp +++ b/src/terrain_3d_editor.cpp @@ -2,99 +2,105 @@ #include #include -#include #include "logger.h" #include "terrain_3d_editor.h" +#include "terrain_3d_storage.h" #include "terrain_3d_util.h" /////////////////////////// // Private Functions /////////////////////////// -void Terrain3DEditor::_region_modified(const Vector3 &p_global_position, const Vector2 &p_height_range) { - Vector2i region_loc = _terrain->get_storage()->get_region_location(p_global_position); - Terrain3DStorage::RegionSize region_size = _terrain->get_storage()->get_region_size(); - +// Sends the whole region aabb to edited_area +void Terrain3DEditor::_send_region_aabb(const Vector2i &p_region_loc, const Vector2 &p_height_range) { + Terrain3D::RegionSize region_size = _terrain->get_region_size(); AABB edited_area; - edited_area.position = Vector3(region_loc.x * region_size, p_height_range.x, region_loc.y * region_size); + edited_area.position = Vector3(p_region_loc.x * region_size, p_height_range.x, p_region_loc.y * region_size); edited_area.size = Vector3(region_size, p_height_range.y - p_height_range.x, region_size); edited_area.position *= _terrain->get_mesh_vertex_spacing(); edited_area.size *= _terrain->get_mesh_vertex_spacing(); - - _modified = true; _terrain->get_storage()->add_edited_area(edited_area); } -void Terrain3DEditor::_operate_region(const Vector3 &p_global_position) { - bool has_region = _terrain->get_storage()->has_region(p_global_position); - bool modified = false; +// Process location to add new region, mark as deleted, or just retrieve +Ref Terrain3DEditor::_operate_region(const Vector2i &p_region_loc) { + bool changed = false; Vector2 height_range; + Terrain3DStorage *storage = _terrain->get_storage(); - if (_operation == ADD) { - if (!has_region) { - _terrain->get_storage()->add_region(p_global_position); - modified = true; + // Check if in bounds, limiting errors + bool can_print = false; + uint64_t ticks = Time::get_singleton()->get_ticks_msec(); + if (ticks - _last_region_bounds_error > 1000) { + _last_region_bounds_error = ticks; + can_print = true; + } + if (storage->get_region_map_index(p_region_loc) < 0) { + if (can_print) { + LOG(ERROR, "Location ", p_region_loc, " out of bounds. Max: ", + -Terrain3DStorage::REGION_MAP_SIZE / 2, " to ", Terrain3DStorage::REGION_MAP_SIZE / 2 - 1); } - } else { - if (has_region) { - int region_id = _terrain->get_storage()->get_region_id(p_global_position); - Ref height_map = _terrain->get_storage()->get_map_region(TYPE_HEIGHT, region_id); - height_range = Util::get_min_max(height_map); + return Ref(); + } + + // Get Region & dump data if debug + Ref region = storage->get_region(p_region_loc); + if (can_print) { + LOG(DEBUG, "Tool: ", _tool, " Op: ", _operation, " processing region ", p_region_loc, ": ", + region.is_valid() ? String::num_uint64(region->get_instance_id()) : "Null"); + if (region.is_valid()) { + LOG(DEBUG, region->get_data()); + } + } - _terrain->get_storage()->remove_region(p_global_position); - modified = true; + // Create new region if location is null or deleted + if (region.is_null() || (region.is_valid() && region->is_deleted())) { + // And tool is Add Region, or Height + auto_regions + if ((_tool == REGION && _operation == ADD) || (_tool == HEIGHT && _brush_data["auto_regions"])) { + region = storage->add_region_blank(p_region_loc); + changed = true; + if (region.is_null()) { + // A new region can't be made + LOG(ERROR, "A new region cannot be created"); + return region; + } } } - if (modified) { - _region_modified(p_global_position, height_range); + // If removing region + else if (region.is_valid() && _tool == REGION && _operation == SUBTRACT) { + _original_regions.push_back(region); + height_range = region->get_height_range(); + _terrain->get_storage()->remove_region(region); + _terrain->get_instancer()->force_update_mmis(); + changed = true; + } + + if (changed) { + _added_removed_locations.push_back(p_region_loc); + region->set_modified(true); + _send_region_aabb(p_region_loc, height_range); } + return region; } void Terrain3DEditor::_operate_map(const Vector3 &p_global_position, const real_t p_camera_direction) { LOG(DEBUG_CONT, "Operating at ", p_global_position, " tool type ", _tool, " op ", _operation); - Ref storage = _terrain->get_storage(); - int region_size = storage->get_region_size(); - Vector2i region_vsize = Vector2i(region_size, region_size); - int region_id = storage->get_region_id(p_global_position); - if (region_id == -1) { - if (!_brush_data["auto_regions"] || _tool != HEIGHT) { - return; - } else { - LOG(DEBUG, "No region to operate on, attempting to add"); - storage->add_region(p_global_position); - region_size = storage->get_region_size(); - region_id = storage->get_region_id(p_global_position); - if (region_id == -1) { - LOG(ERROR, "Failed to add region, no region to operate on"); - return; - } - _region_modified(p_global_position); - } + + MapType map_type = _get_map_type(); + if (map_type == TYPE_MAX) { + LOG(ERROR, "Invalid tool selected"); + return; } - MapType map_type; - switch (_tool) { - case HEIGHT: - case INSTANCER: - map_type = TYPE_HEIGHT; - break; - case TEXTURE: - case AUTOSHADER: - case HOLES: - case NAVIGATION: - case ANGLE: - case SCALE: - map_type = TYPE_CONTROL; - break; - case COLOR: - case ROUGHNESS: - map_type = TYPE_COLOR; - break; - default: - LOG(ERROR, "Invalid tool selected"); - return; + int region_size = _terrain->get_region_size(); + Vector2i region_vsize = Vector2i(region_size, region_size); + + // If no region and can't add one, skip whole function. Checked again later + Terrain3DStorage *storage = _terrain->get_storage(); + if (!storage->has_regionp(p_global_position) && (!_brush_data["auto_regions"] || _tool != HEIGHT)) { + return; } Ref brush_image = _brush_data["brush_image"]; @@ -158,12 +164,10 @@ void Terrain3DEditor::_operate_map(const Vector3 &p_global_position, const real_ } else { _terrain->get_instancer()->remove_instances(p_global_position, _brush_data); } - _modified = true; return; } // MAP Operations - Ref map = storage->get_map_region(map_type, region_id); real_t vertex_spacing = _terrain->get_mesh_vertex_spacing(); for (real_t x = 0.f; x < brush_size; x += vertex_spacing) { for (real_t y = 0.f; y < brush_size; y += vertex_spacing) { @@ -172,24 +176,16 @@ void Terrain3DEditor::_operate_map(const Vector3 &p_global_position, const real_ Vector3(p_global_position.x + brush_offset.x + .5f, p_global_position.y, p_global_position.z + brush_offset.y + .5f); - // If we're brushing across a region boundary, possibly add a region, and get the other map - int new_region_id = storage->get_region_id(brush_global_position); - if (new_region_id == -1) { - if (!_brush_data["auto_regions"] || _tool != HEIGHT) { - continue; - } - Error err = storage->add_region(brush_global_position); - if (err) { - continue; - } - new_region_id = storage->get_region_id(brush_global_position); - _region_modified(brush_global_position); + // Get region for current brush pixel global position + Vector2i region_loc = storage->get_region_location(brush_global_position); + Ref region = _operate_region(region_loc); + // If no region and can't make one, skip + if (region.is_null()) { + continue; } - if (new_region_id != region_id) { - region_id = new_region_id; - map = storage->get_map_region(map_type, region_id); - } + // Get map for this region and tool + Ref map = region->get_map(map_type); // Identify position on map image Vector2 uv_position = _get_uv_position(brush_global_position, region_size, vertex_spacing); @@ -295,7 +291,9 @@ void Terrain3DEditor::_operate_map(const Vector3 &p_global_position, const real_ break; } dest = Color(destf, 0.f, 0.f, 1.f); - storage->update_heights(destf); + region->update_height(destf); + // TODO Move this line to a signal sent from above line + storage->update_master_height(destf); edited_position.y = destf; edited_area = edited_area.expand(edited_position); @@ -447,132 +445,144 @@ void Terrain3DEditor::_operate_map(const Vector3 &p_global_position, const real_ break; } } + backup_region(region); map->set_pixelv(map_pixel_position, dest); } } } - _modified = true; + // Regenerate color mipmaps for edited regions + if (map_type == TYPE_COLOR) { + for (int i = 0; i < _edited_regions.size(); i++) { + Ref region = _edited_regions[i]; + region->get_map(map_type)->generate_mipmaps(); + } + } storage->force_update_maps(map_type); storage->add_edited_area(edited_area); } -Dictionary Terrain3DEditor::_get_undo_data() const { - Dictionary data; - if (_tool < 0 || _tool >= TOOL_MAX) { - return data; - } - switch (_tool) { - case REGION: - LOG(DEBUG, "Storing region locations"); - data["region_locations"] = _terrain->get_storage()->get_region_locations().duplicate(); - if (_operation == SUBTRACT) { - data["height_map"] = _terrain->get_storage()->get_maps_copy(TYPE_HEIGHT); - data["control_map"] = _terrain->get_storage()->get_maps_copy(TYPE_CONTROL); - data["color_map"] = _terrain->get_storage()->get_maps_copy(TYPE_COLOR); - data["height_range"] = _terrain->get_storage()->get_height_range(); - data["edited_area"] = _terrain->get_storage()->get_edited_area(); - } - break; - - case HEIGHT: - LOG(DEBUG, "Storing height maps and range"); - data["region_locations"] = _terrain->get_storage()->get_region_locations().duplicate(); - data["height_map"] = _terrain->get_storage()->get_maps_copy(TYPE_HEIGHT); - data["height_range"] = _terrain->get_storage()->get_height_range(); - data["edited_area"] = _terrain->get_storage()->get_edited_area(); - break; - - case HOLES: - // Holes can remove instances - data["multimeshes"] = _terrain->get_storage()->get_multimeshes().duplicate(true); - LOG(DEBUG, "Storing Multimesh: ", data["multimeshes"]); - case TEXTURE: - case AUTOSHADER: - case NAVIGATION: - LOG(DEBUG, "Storing control maps"); - data["control_map"] = _terrain->get_storage()->get_maps_copy(TYPE_CONTROL); - break; - - case COLOR: - case ROUGHNESS: - LOG(DEBUG, "Storing color maps"); - data["color_map"] = _terrain->get_storage()->get_maps_copy(TYPE_COLOR); - break; - - case INSTANCER: - data["multimeshes"] = _terrain->get_storage()->get_multimeshes().duplicate(true); - LOG(DEBUG, "Storing Multimesh: ", data["multimeshes"]); - break; - - default: - return data; - } - return data; -} - void Terrain3DEditor::_store_undo() { IS_INIT_COND_MESG(_terrain->get_plugin() == nullptr, "_terrain isn't initialized, returning", VOID); if (_tool < 0 || _tool >= TOOL_MAX) { return; } - LOG(INFO, "Storing undo snapshot..."); - EditorUndoRedoManager *undo_redo = _terrain->get_plugin()->get_undo_redo(); - - String action_name = String("Terrain3D ") + OPNAME[_operation] + String(" ") + TOOLNAME[_tool]; - LOG(DEBUG, "Creating undo action: '", action_name, "'"); - undo_redo->create_action(action_name); + LOG(DEBUG, "Finalize undo & redo snapshots"); + Dictionary redo_data; + // Store current locations; Original backed up in start_operation() + redo_data["region_locations"] = _terrain->get_storage()->get_region_locations().duplicate(); + // Store original and current backups of edited regions + _undo_data["edited_regions"] = _original_regions; + redo_data["edited_regions"] = _edited_regions; + + // Store regions that were added or removed + if (_added_removed_locations.size() > 0) { + if (_tool == REGION && _operation == SUBTRACT) { + _undo_data["removed_regions"] = _added_removed_locations; + redo_data["added_regions"] = _added_removed_locations; + } else { + _undo_data["added_regions"] = _added_removed_locations; + redo_data["removed_regions"] = _added_removed_locations; + } + } if (_undo_data.has("edited_area")) { _undo_data["edited_area"] = _terrain->get_storage()->get_edited_area(); LOG(DEBUG, "Updating undo snapshot edited area: ", _undo_data["edited_area"]); } - LOG(DEBUG, "Storing undo snapshot: ", _undo_data); - undo_redo->add_undo_method(this, "apply_undo", _undo_data.duplicate()); + // Store data in Godot's Undo/Redo Manager + LOG(INFO, "Storing undo snapshot..."); + EditorUndoRedoManager *undo_redo = _terrain->get_plugin()->get_undo_redo(); + String action_name = String("Terrain3D ") + OPNAME[_operation] + String(" ") + TOOLNAME[_tool]; + LOG(DEBUG, "Creating undo action: '", action_name, "'"); + undo_redo->create_action(action_name); - LOG(DEBUG, "Setting up redo snapshot..."); - Dictionary redo_set = _get_undo_data(); + LOG(DEBUG, "Storing undo snapshot: ", _undo_data); + undo_redo->add_undo_method(this, "apply_undo", _undo_data); + for (int i = 0; i < _original_regions.size(); i++) { + Ref region = _original_regions[i]; + LOG(DEBUG, "Original Region: ", region->get_data()); + } - LOG(DEBUG, "Storing redo snapshot: ", redo_set); - undo_redo->add_do_method(this, "apply_undo", redo_set); + LOG(DEBUG, "Storing redo snapshot: ", redo_data); + undo_redo->add_do_method(this, "apply_undo", redo_data); + for (int i = 0; i < _edited_regions.size(); i++) { + Ref region = _edited_regions[i]; + LOG(DEBUG, "Edited Region: ", region->get_data()); + } LOG(DEBUG, "Committing undo action"); undo_redo->commit_action(false); } -void Terrain3DEditor::_apply_undo(const Dictionary &p_set) { +void Terrain3DEditor::_apply_undo(const Dictionary &p_data) { IS_INIT_COND_MESG(_terrain->get_plugin() == nullptr, "_terrain isn't initialized, returning", VOID); - LOG(INFO, "Applying Undo/Redo set. Array size: ", p_set.size()); - LOG(DEBUG, "Apply undo received: ", p_set); - - Array keys = p_set.keys(); - for (int i = 0; i < keys.size(); i++) { - String key = keys[i]; - if (key == "region_offsets") { - _terrain->get_storage()->set_region_locations(p_set[key]); - } else if (key == "height_map") { - _terrain->get_storage()->set_maps(TYPE_HEIGHT, p_set[key]); - } else if (key == "control_map") { - _terrain->get_storage()->set_maps(TYPE_CONTROL, p_set[key]); - } else if (key == "color_map") { - _terrain->get_storage()->set_maps(TYPE_COLOR, p_set[key]); - } else if (key == "height_range") { - _terrain->get_storage()->set_height_range(p_set[key]); + LOG(INFO, "Applying Undo/Redo data"); + + Terrain3DStorage *storage = _terrain->get_storage(); + + if (p_data.has("edited_regions")) { + Util::print_arr("Edited regions", p_data["edited_regions"]); + TypedArray undo_regions = p_data["edited_regions"]; + LOG(DEBUG, "Backup has ", undo_regions.size(), " edited regions"); + for (int i = 0; i < undo_regions.size(); i++) { + Ref region = undo_regions[i]; + if (region.is_null()) { + LOG(ERROR, "Null region saved in undo data. Please report this error."); + continue; + } + region->sanitize_maps(); // Live data may not have some maps so must be sanitized + Dictionary regions = storage->get_regions_all(); + regions[region->get_location()] = region; + region->set_modified(true); + LOG(DEBUG, "Edited: ", region->get_data()); + } + } + + if (p_data.has("added_regions")) { + LOG(DEBUG, "Added regions: ", p_data["added_regions"]); + TypedArray region_locs = p_data["added_regions"]; + for (int i = 0; i < region_locs.size(); i++) { + storage->set_region_deleted(region_locs[i], true); + storage->set_region_modified(region_locs[i], true); + LOG(DEBUG, "Marking region: ", region_locs[i], " +deleted, +modified"); + } + } + if (p_data.has("removed_regions")) { + LOG(DEBUG, "Removed regions: ", p_data["removed_regions"]); + TypedArray region_locs = p_data["removed_regions"]; + for (int i = 0; i < region_locs.size(); i++) { + storage->set_region_deleted(region_locs[i], false); + storage->set_region_modified(region_locs[i], true); + LOG(DEBUG, "Marking region: ", region_locs[i], " -deleted, +modified"); + } + } + + // After all regions are in place, reset the region map, which also calls update_maps + if (p_data.has("region_locations")) { + // Load w/ duplicate or it gets a bit wonky undoing removed regions w/ saves + _terrain->get_storage()->set_region_locations(p_data["region_locations"].duplicate()); + Array locations = storage->get_region_locations(); + LOG(DEBUG, "Locations(", locations.size(), "): ", locations); + } + + /* else if (key == "height_range") { + //_terrain->get_storage()->set_height_range(p_set[key]); } else if (key == "edited_area") { _terrain->get_storage()->clear_edited_area(); _terrain->get_storage()->add_edited_area(p_set[key]); } else if (key == "multimeshes") { - _terrain->get_storage()->set_multimeshes(p_set[key]); + //_terrain->get_storage()->set_multimeshes(p_set[key]); } } + */ + storage->force_update_maps(); + _terrain->get_instancer()->force_update_mmis(); if (_terrain->get_plugin()->has_method("update_grid")) { LOG(DEBUG, "Calling GDScript update_grid()"); _terrain->get_plugin()->call("update_grid"); } - - _pending_undo = false; - _modified = false; } /////////////////////////// @@ -648,24 +658,24 @@ void Terrain3DEditor::set_tool(const Tool p_tool) { void Terrain3DEditor::start_operation(const Vector3 &p_global_position) { IS_STORAGE_INIT_MESG("Terrain isn't initialized", VOID); LOG(INFO, "Setting up undo snapshot..."); - _undo_data.clear(); - _undo_data = _get_undo_data(); - _pending_undo = true; - _modified = false; + _undo_data = Dictionary(); // New pointer instead of clear + _undo_data["region_locations"] = _terrain->get_storage()->get_region_locations().duplicate(); + _is_operating = true; + _original_regions = TypedArray(); // New pointers instead of clear + _edited_regions = TypedArray(); + _added_removed_locations = TypedArray(); // Reset counter at start to ensure first click places an instance _terrain->get_instancer()->reset_instance_counter(); _terrain->get_storage()->clear_edited_area(); _operation_position = p_global_position; _operation_movement = Vector3(); - if (_tool == REGION) { - _operate_region(p_global_position); - } } // Called on mouse movement with left mouse button down void Terrain3DEditor::operate(const Vector3 &p_global_position, const real_t p_camera_direction) { IS_STORAGE_INIT_MESG("Terrain isn't initialized", VOID); - if (!_pending_undo) { + if (!_is_operating) { + LOG(ERROR, "Run start_operation() before operating"); return; } _operation_movement = p_global_position - _operation_position; @@ -673,7 +683,7 @@ void Terrain3DEditor::operate(const Vector3 &p_global_position, const real_t p_c // Convolve the last 8 movement events, we dont clear on mouse release // so as to make repeated mouse strokes in the same direction consistent - _operation_movement_history.append(_operation_movement); + _operation_movement_history.push_back(_operation_movement); if (_operation_movement_history.size() > 8) { _operation_movement_history.pop_front(); } @@ -684,21 +694,43 @@ void Terrain3DEditor::operate(const Vector3 &p_global_position, const real_t p_c _operation_movement *= 0.125; // 1/8th if (_tool == REGION) { - _operate_region(p_global_position); + _operate_region(_terrain->get_storage()->get_region_location(p_global_position)); } else if (_tool >= 0 && _tool < TOOL_MAX) { _operate_map(p_global_position, p_camera_direction); } } +void Terrain3DEditor::backup_region(const Ref &p_region) { + if (_is_operating && p_region.is_valid() && !p_region->is_edited()) { + LOG(DEBUG, "Storing original copy of region: ", p_region->get_location()); + _original_regions.push_back(p_region->duplicate(true)); + _edited_regions.push_back(p_region); + p_region->set_edited(true); + p_region->set_modified(true); + } +} + // Called on left mouse button released void Terrain3DEditor::stop_operation() { IS_STORAGE_INIT_MESG("Terrain isn't initialized", VOID); - if (_pending_undo && _modified) { + // If undo was created and terrain actually modified, store it + LOG(DEBUG, "Backed up regions: ", _original_regions.size(), ", Edited regions: ", _edited_regions.size(), + ", Added/Removed regions: ", _added_removed_locations.size()); + if (_is_operating && (!_added_removed_locations.is_empty() || !_edited_regions.is_empty())) { + for (int i = 0; i < _edited_regions.size(); i++) { + Ref region = _edited_regions[i]; + region->set_edited(false); + LOG(DEBUG, "Edited region: ", region->get_data()); + // Make duplicate for redo backup + _edited_regions[i] = region->duplicate(true); + } _store_undo(); - _pending_undo = false; - _modified = false; - _terrain->get_storage()->clear_edited_area(); } + _original_regions = TypedArray(); //New pointers instead of clear + _edited_regions = TypedArray(); + _added_removed_locations = TypedArray(); + _terrain->get_storage()->clear_edited_area(); + _is_operating = false; } /////////////////////////// @@ -735,9 +767,10 @@ void Terrain3DEditor::_bind_methods() { ClassDB::bind_method(D_METHOD("set_operation", "operation"), &Terrain3DEditor::set_operation); ClassDB::bind_method(D_METHOD("get_operation"), &Terrain3DEditor::get_operation); ClassDB::bind_method(D_METHOD("start_operation", "position"), &Terrain3DEditor::start_operation); + ClassDB::bind_method(D_METHOD("is_operating"), &Terrain3DEditor::is_operating); ClassDB::bind_method(D_METHOD("operate", "position", "camera_direction"), &Terrain3DEditor::operate); + ClassDB::bind_method(D_METHOD("backup_region", "region"), &Terrain3DEditor::backup_region); ClassDB::bind_method(D_METHOD("stop_operation"), &Terrain3DEditor::stop_operation); - ClassDB::bind_method(D_METHOD("is_operating"), &Terrain3DEditor::is_operating); - ClassDB::bind_method(D_METHOD("apply_undo", "maps"), &Terrain3DEditor::_apply_undo); + ClassDB::bind_method(D_METHOD("apply_undo", "data"), &Terrain3DEditor::_apply_undo); } diff --git a/src/terrain_3d_editor.h b/src/terrain_3d_editor.h index ab7e4c5b..a590cd50 100644 --- a/src/terrain_3d_editor.h +++ b/src/terrain_3d_editor.h @@ -7,6 +7,7 @@ #include #include "terrain_3d.h" +#include "terrain_3d_region.h" using namespace godot; @@ -73,22 +74,25 @@ class Terrain3DEditor : public Object { Vector3 _operation_position = Vector3(); Vector3 _operation_movement = Vector3(); Array _operation_movement_history; - bool _pending_undo = false; - bool _modified = false; + bool _is_operating = false; + uint64_t _last_region_bounds_error = 0; + TypedArray _original_regions; // Queue for undo + TypedArray _edited_regions; // Queue for redo + TypedArray _added_removed_locations; // Queue for added/removed locations AABB _modified_area; Dictionary _undo_data; // See _get_undo_data for definition uint64_t _last_pen_tick = 0; - void _region_modified(const Vector3 &p_global_position, const Vector2 &p_height_range = Vector2()); - void _operate_region(const Vector3 &p_global_position); + void _send_region_aabb(const Vector2i &p_region_loc, const Vector2 &p_height_range = Vector2()); + Ref _operate_region(const Vector2i &p_region_loc); void _operate_map(const Vector3 &p_global_position, const real_t p_camera_direction); + MapType _get_map_type() const; bool _is_in_bounds(const Vector2i &p_position, const Vector2i &p_max_position) const; Vector2 _get_uv_position(const Vector3 &p_global_position, const int p_region_size, const real_t p_vertex_spacing) const; Vector2 _get_rotated_uv(const Vector2 &p_uv, const real_t p_angle) const; - Dictionary _get_undo_data() const; void _store_undo(); - void _apply_undo(const Dictionary &p_set); + void _apply_undo(const Dictionary &p_data); public: Terrain3DEditor() {} @@ -104,9 +108,10 @@ class Terrain3DEditor : public Object { Operation get_operation() const { return _operation; } void start_operation(const Vector3 &p_global_position); + bool is_operating() const { return _is_operating; } void operate(const Vector3 &p_global_position, const real_t p_camera_direction); + void backup_region(const Ref &p_region); void stop_operation(); - bool is_operating() const { return _pending_undo; } protected: static void _bind_methods(); @@ -117,6 +122,29 @@ VARIANT_ENUM_CAST(Terrain3DEditor::Tool); /// Inline functions +inline MapType Terrain3DEditor::_get_map_type() const { + switch (_tool) { + case HEIGHT: + case INSTANCER: + return TYPE_HEIGHT; + break; + case TEXTURE: + case AUTOSHADER: + case HOLES: + case NAVIGATION: + case ANGLE: + case SCALE: + return TYPE_CONTROL; + break; + case COLOR: + case ROUGHNESS: + return TYPE_COLOR; + break; + default: + return TYPE_MAX; + } +} + inline bool Terrain3DEditor::_is_in_bounds(const Vector2i &p_position, const Vector2i &p_max_position) const { bool more_than_min = p_position.x >= 0 && p_position.y >= 0; bool less_than_max = p_position.x < p_max_position.x && p_position.y < p_max_position.y; @@ -134,7 +162,7 @@ inline Vector2 Terrain3DEditor::_get_uv_position(const Vector3 &p_global_positio inline Vector2 Terrain3DEditor::_get_rotated_uv(const Vector2 &p_uv, const real_t p_angle) const { Vector2 rotation_offset = Vector2(0.5f, 0.5f); Vector2 uv = (p_uv - rotation_offset).rotated(p_angle) + rotation_offset; - return uv.clamp(Vector2(0.f, 0.f), Vector2(1.f, 1.f)); + return uv.clamp(V2_ZERO, Vector2(1.f, 1.f)); } #endif // TERRAIN3D_EDITOR_CLASS_H diff --git a/src/terrain_3d_instancer.cpp b/src/terrain_3d_instancer.cpp index c9a6011b..7b44e5ca 100644 --- a/src/terrain_3d_instancer.cpp +++ b/src/terrain_3d_instancer.cpp @@ -4,29 +4,18 @@ #include "logger.h" #include "terrain_3d_instancer.h" +#include "terrain_3d_region.h" #include "terrain_3d_util.h" /////////////////////////// // Private Functions /////////////////////////// -void Terrain3DInstancer::_rebuild_mmis() { - destroy(); - _update_mmis(); -} - // Creates MMIs based on stored Multimesh data void Terrain3DInstancer::_update_mmis(const Vector2i &p_region_loc, const int p_mesh_id) { IS_STORAGE_INIT(VOID); LOG(INFO, "Updating MMIs for ", (p_region_loc.x == INT32_MAX) ? "all regions" : "region " + String(p_region_loc), (p_mesh_id == -1) ? ", all meshes" : ", mesh " + String::num_int64(p_mesh_id)); - // Get region multimeshes dictionary - Dictionary region_dict = _terrain->get_storage()->get_multimeshes(); - LOG(DEBUG, "Multimeshes: ", region_dict); - if (region_dict.has(Variant())) { - region_dict.erase(Variant()); - LOG(WARN, "Removed errant null in MM dictionary"); - } if (_mmis.has(Variant())) { _mmis.erase(Variant()); LOG(WARN, "Removed errant null in MMI dictionary"); @@ -34,14 +23,19 @@ void Terrain3DInstancer::_update_mmis(const Vector2i &p_region_loc, const int p_ // For specified region_location, or max for all Array region_locations; - if (p_region_loc == Vector2i(INT32_MAX, INT32_MAX)) { - region_locations = region_dict.keys(); + if (p_region_loc.x == INT32_MAX) { + region_locations = _terrain->get_storage()->get_region_locations(); } else { region_locations.push_back(p_region_loc); } for (int r = 0; r < region_locations.size(); r++) { Vector2i region_loc = region_locations[r]; - Dictionary mesh_dict = region_dict.get(region_loc, Dictionary()); + Ref region = _terrain->get_storage()->get_region(region_loc); + if (region.is_null()) { + LOG(WARN, "Errant null region found at: ", region_loc); + continue; + } + Dictionary mesh_dict = region->get_multimeshes(); LOG(DEBUG, "Updating MMIs from: ", region_loc); // For specified mesh id in that region, or -1 for all @@ -94,7 +88,7 @@ void Terrain3DInstancer::_update_mmis(const Vector2i &p_region_loc, const int p_ LOG(DEBUG, "No MMI found, creating new MultiMeshInstance3D, attaching to tree"); mmi = memnew(MultiMeshInstance3D); mmi->set_as_top_level(true); - _terrain->add_child(mmi, true); + _terrain->get_mmi_parent()->add_child(mmi, true); _mmis[mmi_key] = mmi; LOG(DEBUG, _mmis); } @@ -106,13 +100,13 @@ void Terrain3DInstancer::_update_mmis(const Vector2i &p_region_loc, const int p_ mmi->set_global_transform(Transform3D()); } } + LOG(DEBUG, "mm: ", mesh_dict); } - LOG(DEBUG, "mm: ", _terrain->get_storage()->get_multimeshes()); LOG(DEBUG, "_mmis: ", _mmis); } void Terrain3DInstancer::_destroy_mmi_by_region_id(const int p_region_id, const int p_mesh_id) { - Vector2i region_loc = _terrain->get_storage()->get_region_location_from_id(p_region_id); + Vector2i region_loc = _terrain->get_storage()->get_region_locationi(p_region_id); _destroy_mmi_by_location(region_loc, p_mesh_id); } @@ -128,14 +122,25 @@ void Terrain3DInstancer::_destroy_mmi_by_location(const Vector2i &p_region_loc, LOG(DEBUG, "Deleting MMI, success: ", result); } +void Terrain3DInstancer::_backup_regionl(const Vector2i &p_region_loc) { + if (_terrain->get_storage() != nullptr) { + Ref region = _terrain->get_storage()->get_region(p_region_loc); + _backup_region(region); + } +} + +void Terrain3DInstancer::_backup_region(const Ref &p_region) { + if (_terrain->get_editor() != nullptr) { + _terrain->get_editor()->backup_region(p_region); + } else { + p_region->set_modified(true); + } +} + /////////////////////////// // Public Functions /////////////////////////// -Terrain3DInstancer::~Terrain3DInstancer() { - destroy(); -} - void Terrain3DInstancer::initialize(Terrain3D *p_terrain) { if (p_terrain) { _terrain = p_terrain; @@ -157,29 +162,29 @@ void Terrain3DInstancer::destroy() { void Terrain3DInstancer::clear_by_mesh(const int p_mesh_id) { LOG(INFO, "Deleting Multimeshes in all regions with mesh_id: ", p_mesh_id); - Dictionary region_dict = _terrain->get_storage()->get_multimeshes(); - Array locations = region_dict.keys(); - for (int i = 0; i < locations.size(); i++) { - clear_by_location(locations[i], p_mesh_id); + Array region_locations = _terrain->get_storage()->get_region_locations(); + for (int i = 0; i < region_locations.size(); i++) { + clear_by_location(region_locations[i], p_mesh_id); } } void Terrain3DInstancer::clear_by_region_id(const int p_region_id, const int p_mesh_id) { - Vector2i region_loc = _terrain->get_storage()->get_region_location_from_id(p_region_id); + Vector2i region_loc = _terrain->get_storage()->get_region_locationi(p_region_id); clear_by_location(region_loc, p_mesh_id); } void Terrain3DInstancer::clear_by_location(const Vector2i &p_region_loc, const int p_mesh_id) { LOG(INFO, "Deleting Multimeshes w/ mesh_id: ", p_mesh_id, " in region: ", p_region_loc); - Dictionary region_dict = _terrain->get_storage()->get_multimeshes(); - LOG(DEBUG, "Original region_dict: ", region_dict); - Dictionary mesh_dict = region_dict[p_region_loc]; - mesh_dict.erase(p_mesh_id); - if (mesh_dict.is_empty()) { - LOG(DEBUG, "No more multimeshes in region, removing region dictionary"); - region_dict.erase(p_region_loc); - } - LOG(DEBUG, "Final region_dict: ", region_dict); + Ref region = _terrain->get_storage()->get_region(p_region_loc); + if (region.is_null()) { + LOG(WARN, "No region found at: ", p_region_loc); + return; + } + Dictionary mesh_dict = region->get_multimeshes(); + if (mesh_dict.has(p_mesh_id)) { + _backup_region(region); + mesh_dict.erase(p_mesh_id); + } _destroy_mmi_by_location(p_region_loc, p_mesh_id); } @@ -266,9 +271,8 @@ void Terrain3DInstancer::add_instances(const Vector3 &p_global_position, const D real_t t_scale = CLAMP(fixed_scale + random_scale * (2.f * UtilityFunctions::randf() - 1.f), 0.01f, 10.f); t = t.scaled(Vector3(t_scale, t_scale, t_scale)); - // Position - real_t offset = height_offset + mesh_asset->get_height_offset() + - random_height * (2.f * UtilityFunctions::randf() - 1.f); + // Position. mesh_asset height offset added in add_transforms + real_t offset = height_offset + random_height * (2.f * UtilityFunctions::randf() - 1.f); position += t.basis.get_column(1) * offset; // Offset along UP axis t = t.translated(position); @@ -283,10 +287,8 @@ void Terrain3DInstancer::add_instances(const Vector3 &p_global_position, const D // Append multimesh if (xforms.size() > 0) { - Vector2i region_loc = _terrain->get_storage()->get_region_location(p_global_position); - append_multimesh(region_loc, mesh_id, xforms, colors); + add_transforms(mesh_id, xforms, colors); } - _terrain->get_storage()->set_modified(); } void Terrain3DInstancer::remove_instances(const Vector3 &p_global_position, const Dictionary &p_params) { @@ -344,7 +346,6 @@ void Terrain3DInstancer::remove_instances(const Vector3 &p_global_position, cons } else { append_multimesh(region_loc, mesh_id, xforms, colors, true); } - _terrain->get_storage()->set_modified(); } void Terrain3DInstancer::add_multimesh(const int p_mesh_id, const Ref &p_multimesh, const Transform3D &p_xform) { @@ -408,8 +409,6 @@ void Terrain3DInstancer::add_transforms(const int p_mesh_id, const TypedArrayget_storage()->set_modified(); } // Appends new transforms to existing multimeshes @@ -456,14 +455,16 @@ void Terrain3DInstancer::append_multimesh(const Vector2i &p_region_loc, const in mm->set_instance_transform(i + old_count, p_xforms[i]); mm->set_instance_color(i + old_count, p_colors[i]); } - LOG(DEBUG_CONT, "Setting multimesh in region: ", p_region_loc, ", mesh_id: ", p_mesh_id, " instance count: ", mm->get_instance_count(), " mm: ", mm); - Dictionary region_dict = _terrain->get_storage()->get_multimeshes(); - Dictionary mesh_dict = region_dict.get(p_region_loc, Dictionary()); - // Assign into dictionaries in case these are new resources + LOG(DEBUG_CONT, "Setting multimesh in region: ", p_region_loc, ", mesh_id: ", p_mesh_id, " instance count: ", mm->get_instance_count(), " mm: ", mm); + Ref region = _terrain->get_storage()->get_region(p_region_loc); + if (region.is_null()) { + LOG(WARN, "No region found at: ", p_region_loc); + return; + } + _backup_region(region); + Dictionary mesh_dict = region->get_multimeshes(); mesh_dict[p_mesh_id] = mm; - region_dict[p_region_loc] = mesh_dict; - _update_mmis(p_region_loc, p_mesh_id); } @@ -472,12 +473,16 @@ void Terrain3DInstancer::update_transforms(const AABB &p_aabb) { IS_STORAGE_INIT_MESG("Instancer isn't initialized.", VOID); LOG(DEBUG_CONT, "Updating transforms for all meshes within ", p_aabb); - Dictionary region_dict = _terrain->get_storage()->get_multimeshes(); - Array regions = region_dict.keys(); + Array region_locations = _terrain->get_storage()->get_region_locations(); Rect2 brush_rect = aabb2rect(p_aabb); - for (int r = 0; r < regions.size(); r++) { - Vector2i region_loc = regions[r]; - int region_size = _terrain->get_storage()->get_region_size(); + for (int r = 0; r < region_locations.size(); r++) { + Vector2i region_loc = region_locations[r]; + Ref region = _terrain->get_storage()->get_region(region_loc); + if (region.is_null()) { + LOG(WARN, "No region found at: ", region_loc); + continue; + } + int region_size = _terrain->get_region_size(); Rect2 region_rect; region_rect.set_position(region_loc * region_size); region_rect.set_size(Vector2(region_size, region_size)); @@ -485,7 +490,7 @@ void Terrain3DInstancer::update_transforms(const AABB &p_aabb) { // If specified area includes this region, update all MMs within if (brush_rect.intersects(region_rect)) { - Dictionary mesh_dict = region_dict.get(region_loc, Dictionary()); + Dictionary mesh_dict = region->get_multimeshes(); LOG(DEBUG_CONT, "Region ", region_loc, " intersect AABB and contains ", mesh_dict.size(), " mesh types"); // For all mesh ids for (int m = 0; m < mesh_dict.keys().size(); m++) { @@ -528,29 +533,37 @@ void Terrain3DInstancer::swap_ids(const int p_src_id, const int p_dst_id) { LOG(INFO, "Swapping IDs of multimeshes: ", p_src_id, " and ", p_dst_id); if (p_src_id >= 0 && p_src_id < asset_count && p_dst_id >= 0 && p_dst_id < asset_count) { // Change id keys in storage mm dictionary - Dictionary multimeshes = _terrain->get_storage()->get_multimeshes(); - Array mm_keys = multimeshes.keys(); - for (int i = 0; i < mm_keys.size(); i++) { - Vector2i region_loc = mm_keys[i]; - Dictionary mesh_dict = multimeshes[region_loc]; + Array region_locations = _terrain->get_storage()->get_region_locations(); + for (int i = 0; i < region_locations.size(); i++) { + Vector2i region_loc = region_locations[i]; + Ref region = _terrain->get_storage()->get_region(region_loc); + if (region.is_null()) { + LOG(WARN, "No region found at: ", region_loc); + return; + } + Dictionary mesh_dict = region->get_multimeshes(); // mesh_dict could have src, src&dst, dst or nothing. All 4 must be considered // Pop out any existing MMs Ref mm_src; Ref mm_dst; if (mesh_dict.has(p_src_id)) { + _backup_region(region); mm_src = mesh_dict[p_src_id]; mesh_dict.erase(p_src_id); } if (mesh_dict.has(p_dst_id)) { + _backup_region(region); mm_dst = mesh_dict[p_dst_id]; mesh_dict.erase(p_dst_id); } // If src is ok, insert into dst slot if (mm_src.is_valid()) { + _backup_region(region); mesh_dict[p_dst_id] = mm_src; } // If dst is ok, insert into src slot if (mm_dst.is_valid()) { + _backup_region(region); mesh_dict[p_src_id] = mm_dst; } LOG(DEBUG, "Swapped multimesh ids at: ", region_loc); @@ -588,7 +601,12 @@ Ref Terrain3DInstancer::get_multimesh(const Vector3 &p_global_positio Ref Terrain3DInstancer::get_multimesh(const Vector2i &p_region_loc, const int p_mesh_id) const { IS_STORAGE_INIT(Ref()); - Dictionary mesh_dict = _terrain->get_storage()->get_multimeshes().get(p_region_loc, Dictionary()); + Ref region = _terrain->get_storage()->get_region(p_region_loc); + if (region.is_null()) { + LOG(WARN, "No region found at: ", p_region_loc); + return Ref(); + } + Dictionary mesh_dict = region->get_multimeshes(); Ref mm = mesh_dict.get(p_mesh_id, Ref()); LOG(DEBUG_CONT, "Retrieving MultiMesh at region: ", p_region_loc, " mesh_id: ", p_mesh_id, " : ", mm); return mm; @@ -620,6 +638,11 @@ void Terrain3DInstancer::set_cast_shadows(const int p_mesh_id, const GeometryIns } } +void Terrain3DInstancer::force_update_mmis() { + destroy(); + _update_mmis(); +} + void Terrain3DInstancer::print_multimesh_buffer(MultiMeshInstance3D *p_mmi) const { if (p_mmi == nullptr) { return; @@ -658,4 +681,5 @@ void Terrain3DInstancer::_bind_methods() { ClassDB::bind_method(D_METHOD("update_transforms", "aabb"), &Terrain3DInstancer::update_transforms); ClassDB::bind_method(D_METHOD("get_mmis"), &Terrain3DInstancer::get_mmis); ClassDB::bind_method(D_METHOD("set_cast_shadows", "mesh_id", "mode"), &Terrain3DInstancer::set_cast_shadows); + ClassDB::bind_method(D_METHOD("force_update_mmis"), &Terrain3DInstancer::force_update_mmis); } diff --git a/src/terrain_3d_instancer.h b/src/terrain_3d_instancer.h index 8da07877..7db0dc72 100644 --- a/src/terrain_3d_instancer.h +++ b/src/terrain_3d_instancer.h @@ -20,8 +20,8 @@ class Terrain3DInstancer : public Object { Terrain3D *_terrain = nullptr; - // MM Resources stored in Terrain3DStorage::_multimeshes as - // Dictionary[region_location:Vector2i] -> Dictionary[mesh_id:int] -> MultiMesh + // MM Resources stored in Terrain3DRegion::_multimeshes as + // Dictionary[mesh_id:int] -> MultiMesh // MMI Objects attached to tree, freed in destructor, stored as // Dictionary[Vector3i(region_location.x, region_location.y, mesh_id)] -> MultiMeshInstance3D Dictionary _mmis; @@ -29,14 +29,15 @@ class Terrain3DInstancer : public Object { uint32_t _instance_counter = 0; int _get_instace_count(const real_t p_density); - void _rebuild_mmis(); - void _update_mmis(const Vector2i &p_region_loc = Vector2i(INT32_MAX, INT32_MAX), const int p_mesh_id = -1); + void _update_mmis(const Vector2i &p_region_loc = V2I_MAX, const int p_mesh_id = -1); void _destroy_mmi_by_region_id(const int p_region, const int p_mesh_id); void _destroy_mmi_by_location(const Vector2i &p_region_loc, const int p_mesh_id); + void _backup_regionl(const Vector2i &p_region_loc); + void _backup_region(const Ref &p_region); public: Terrain3DInstancer() {} - ~Terrain3DInstancer(); + ~Terrain3DInstancer() { destroy(); } void initialize(Terrain3D *p_terrain); void destroy(); @@ -60,6 +61,7 @@ class Terrain3DInstancer : public Object { void set_cast_shadows(const int p_mesh_id, const GeometryInstance3D::ShadowCastingSetting p_cast_shadows); void reset_instance_counter() { _instance_counter = 0; } + void force_update_mmis(); void print_multimesh_buffer(MultiMeshInstance3D *p_mmi) const; protected: diff --git a/src/terrain_3d_material.cpp b/src/terrain_3d_material.cpp index e819a733..1f64bb91 100644 --- a/src/terrain_3d_material.cpp +++ b/src/terrain_3d_material.cpp @@ -298,18 +298,12 @@ void Terrain3DMaterial::_update_maps() { IS_STORAGE_INIT(VOID); LOG(DEBUG_CONT, "Updating maps in shader"); - Ref storage = _terrain->get_storage(); - RS->material_set_param(_material, "_height_maps", storage->get_height_maps_rid()); - RS->material_set_param(_material, "_control_maps", storage->get_control_maps_rid()); - RS->material_set_param(_material, "_color_maps", storage->get_color_maps_rid()); - LOG(DEBUG_CONT, "Height map RID: ", storage->get_height_maps_rid()); - LOG(DEBUG_CONT, "Control map RID: ", storage->get_control_maps_rid()); - LOG(DEBUG_CONT, "Color map RID: ", storage->get_color_maps_rid()); - + Terrain3DStorage *storage = _terrain->get_storage(); PackedInt32Array region_map = storage->get_region_map(); LOG(DEBUG_CONT, "region_map.size(): ", region_map.size()); if (region_map.size() != Terrain3DStorage::REGION_MAP_SIZE * Terrain3DStorage::REGION_MAP_SIZE) { LOG(ERROR, "Expected region_map.size() of ", Terrain3DStorage::REGION_MAP_SIZE * Terrain3DStorage::REGION_MAP_SIZE); + return; } RS->material_set_param(_material, "_region_map", region_map); RS->material_set_param(_material, "_region_map_size", Terrain3DStorage::REGION_MAP_SIZE); @@ -326,11 +320,18 @@ void Terrain3DMaterial::_update_maps() { LOG(DEBUG_CONT, "Region_locations size: ", region_locations.size(), " ", region_locations); RS->material_set_param(_material, "_region_locations", region_locations); - real_t region_size = real_t(storage->get_region_size()); + real_t region_size = real_t(_terrain->get_region_size()); LOG(DEBUG_CONT, "Setting region size in material: ", region_size); RS->material_set_param(_material, "_region_size", region_size); RS->material_set_param(_material, "_region_texel_size", 1.0f / region_size); + RS->material_set_param(_material, "_height_maps", storage->get_height_maps_rid()); + RS->material_set_param(_material, "_control_maps", storage->get_control_maps_rid()); + RS->material_set_param(_material, "_color_maps", storage->get_color_maps_rid()); + LOG(DEBUG_CONT, "Height map RID: ", storage->get_height_maps_rid()); + LOG(DEBUG_CONT, "Control map RID: ", storage->get_control_maps_rid()); + LOG(DEBUG_CONT, "Color map RID: ", storage->get_color_maps_rid()); + real_t spacing = _terrain->get_mesh_vertex_spacing(); LOG(DEBUG_CONT, "Setting mesh vertex spacing in material: ", spacing); RS->material_set_param(_material, "_mesh_vertex_spacing", spacing); diff --git a/src/terrain_3d_region.cpp b/src/terrain_3d_region.cpp new file mode 100644 index 00000000..341520f8 --- /dev/null +++ b/src/terrain_3d_region.cpp @@ -0,0 +1,376 @@ +// Copyright © 2024 Cory Petkovsek, Roope Palmroos, and Contributors. + +#include + +#include "logger.h" +#include "terrain_3d_region.h" +#include "terrain_3d_storage.h" +#include "terrain_3d_util.h" + +///////////////////// +// Public Functions +///////////////////// + +void Terrain3DRegion::set_version(const real_t p_version) { + LOG(INFO, vformat("%.3f", p_version)); + _version = p_version; + if (_version < Terrain3DStorage::CURRENT_VERSION) { + LOG(WARN, "Region ", get_path(), " version ", vformat("%.3f", _version), + " will be updated to ", vformat("%.3f", Terrain3DStorage::CURRENT_VERSION), " upon save"); + } +} + +void Terrain3DRegion::set_map(const MapType p_map_type, const Ref &p_image) { + switch (p_map_type) { + case TYPE_HEIGHT: + set_height_map(p_image); + break; + case TYPE_CONTROL: + set_control_map(p_image); + break; + case TYPE_COLOR: + set_color_map(p_image); + break; + default: + LOG(ERROR, "Requested map type is invalid"); + break; + } +} + +Ref Terrain3DRegion::get_map(const MapType p_map_type) const { + switch (p_map_type) { + case TYPE_HEIGHT: + return get_height_map(); + break; + case TYPE_CONTROL: + return get_control_map(); + break; + case TYPE_COLOR: + return get_color_map(); + break; + default: + LOG(ERROR, "Requested map type is invalid"); + return Ref(); + } +} + +void Terrain3DRegion::set_maps(const TypedArray &p_maps) { + if (p_maps.size() != TYPE_MAX) { + LOG(ERROR, "Expected ", TYPE_MAX - 1, " maps. Received ", p_maps.size()); + return; + } + _region_size = 0; + set_height_map(p_maps[TYPE_HEIGHT]); + set_control_map(p_maps[TYPE_CONTROL]); + set_color_map(p_maps[TYPE_COLOR]); +} + +TypedArray Terrain3DRegion::get_maps() const { + LOG(INFO, "Retrieving maps from region: ", _location); + TypedArray maps; + maps.push_back(_height_map); + maps.push_back(_control_map); + maps.push_back(_color_map); + return maps; +} + +void Terrain3DRegion::set_height_map(const Ref &p_map) { + LOG(INFO, "Setting height map for region: ", (_location.x != INT32_MAX) ? String(_location) : "(new)"); + if (_region_size == 0) { + set_region_size((p_map.is_valid()) ? p_map->get_width() : 0); + } + _height_map = sanitize_map(TYPE_HEIGHT, p_map); + calc_height_range(); +} + +void Terrain3DRegion::set_control_map(const Ref &p_map) { + LOG(INFO, "Setting control map for region: ", (_location.x != INT32_MAX) ? String(_location) : "(new)"); + if (_region_size == 0) { + set_region_size((p_map.is_valid()) ? p_map->get_width() : 0); + } + _control_map = sanitize_map(TYPE_CONTROL, p_map); +} + +void Terrain3DRegion::set_color_map(const Ref &p_map) { + LOG(INFO, "Setting color map for region: ", (_location.x != INT32_MAX) ? String(_location) : "(new)"); + if (_region_size == 0) { + set_region_size((p_map.is_valid()) ? p_map->get_width() : 0); + } + _color_map = sanitize_map(TYPE_COLOR, p_map); + if (!_color_map->has_mipmaps()) { + LOG(DEBUG, "Color map does not have mipmaps. Generating"); + _color_map->generate_mipmaps(); + } +} + +bool Terrain3DRegion::validate_map_size(const Ref &p_map) const { + Vector2i region_sizev = p_map->get_size(); + if (region_sizev.x != region_sizev.y) { + LOG(ERROR, "Image width doesn't match height: ", region_sizev); + return false; + } + if (!is_power_of_2(region_sizev.x) || !is_power_of_2(region_sizev.y)) { + LOG(ERROR, "Image dimensions are not a power of 2: ", region_sizev); + return false; + } + if (region_sizev.x < 64 || region_sizev.y > 4096) { + LOG(ERROR, "Image size out of bounds (64-4096): ", region_sizev); + return false; + } + if (_region_size == 0) { + LOG(ERROR, "Region size is 0, set it or set a map first"); + return false; + } + if (_region_size != region_sizev.x || _region_size != region_sizev.y) { + LOG(ERROR, "Image size doesn't match existing images in this region", region_sizev); + return false; + } + return true; +} + +void Terrain3DRegion::sanitize_maps() { + if (_region_size == 0) { // blank region, no set_*_map has been called + LOG(ERROR, "Set region_size first"); + return; + } + _height_map = sanitize_map(TYPE_HEIGHT, _height_map); + _control_map = sanitize_map(TYPE_CONTROL, _control_map); + _color_map = sanitize_map(TYPE_COLOR, _color_map); +} + +Ref Terrain3DRegion::sanitize_map(const MapType p_map_type, const Ref &p_map) const { + const char *type_str = TYPESTR[p_map_type]; + Image::Format format = FORMAT[p_map_type]; + Color color = COLOR[p_map_type]; + Ref map; + + if (p_map.is_valid()) { + if (validate_map_size(p_map)) { + if (p_map->get_format() == format) { + LOG(DEBUG, "Map type ", type_str, " correct format, size. Mipmaps: ", p_map->has_mipmaps()); + map = p_map; + } else { + LOG(DEBUG, "Provided ", type_str, " map wrong format: ", p_map->get_format(), ". Converting copy to: ", format); + map.instantiate(); + map->copy_from(p_map); + map->convert(format); + if (map->get_format() != format) { + LOG(DEBUG, "Cannot convert image to format: ", format, ". Creating blank "); + map.unref(); + } + } + } else { + LOG(DEBUG, "Provided ", type_str, " map wrong size: ", p_map->get_size(), ". Creating blank"); + } + } else { + LOG(DEBUG, "No provided ", type_str, " map. Creating blank"); + } + if (map.is_null()) { + LOG(DEBUG, "Making new image of type: ", type_str, " and generating mipmaps: ", p_map_type == TYPE_COLOR); + return Util::get_filled_image(Vector2i(_region_size, _region_size), color, p_map_type == TYPE_COLOR, format); + } else { + return map; + } +} + +void Terrain3DRegion::set_height_range(const Vector2 &p_range) { + LOG(INFO, vformat("%.2v", p_range)); + if (_height_range != p_range) { + // If initial value, we're loading it from disk, else mark modified + if (_height_range != V2_ZERO) { + _modified = true; + } + _height_range = p_range; + } +} + +void Terrain3DRegion::calc_height_range() { + Vector2 range = Util::get_min_max(_height_map); + if (_height_range != range) { + _height_range = range; + _modified = true; + LOG(DEBUG, "Recalculated new height range: ", _height_range, " for region: ", (_location.x != INT32_MAX) ? String(_location) : "(new)", ". Marking modified"); + } +} + +Error Terrain3DRegion::save(const String &p_path, const bool p_16_bit) { + // Initiate save to external file. The scene will save itself. + if (_location.x == INT32_MAX) { + LOG(ERROR, "Region has not been setup. Location is INT32_MAX. Skipping ", p_path); + } + if (!_modified) { + LOG(DEBUG, "Region ", _location, " not modified. Skipping ", p_path); + return ERR_SKIP; + } + if (p_path.is_empty() && get_path().is_empty()) { + LOG(ERROR, "No valid path provided"); + return ERR_FILE_NOT_FOUND; + } + if (get_path().is_empty() && !p_path.is_empty()) { + LOG(DEBUG, "Setting file path for region ", _location, " to ", p_path); + take_over_path(p_path); + // Set region path and take over the path from any other cached resources, + // incuding those in the undo queue + } + LOG(INFO, "Writing", (p_16_bit) ? " 16-bit" : "", " region ", _location, " to ", get_path()); + set_version(Terrain3DStorage::CURRENT_VERSION); + Error err; + if (p_16_bit) { + Ref original_map; + original_map.instantiate(); + original_map->copy_from(_height_map); + _height_map->convert(Image::FORMAT_RH); + err = ResourceSaver::get_singleton()->save(this, get_path(), ResourceSaver::FLAG_COMPRESS); + _height_map = original_map; + } else { + err = ResourceSaver::get_singleton()->save(this, get_path(), ResourceSaver::FLAG_COMPRESS); + } + if (err == OK) { + _modified = false; + LOG(INFO, "File saved successfully"); + } else { + LOG(ERROR, "Cannot save region file: ", get_path(), ". Error code: ", ERROR, ". Look up @GlobalScope Error enum in the Godot docs"); + } + return err; +} + +void Terrain3DRegion::set_location(const Vector2i &p_location) { + // In the future anywhere they want to put the location might be fine, but because of region_map + // We have a limitation of 16x16 and eventually 45x45. + if (Terrain3DStorage::get_region_map_index(p_location) < 0) { + LOG(ERROR, "Location ", p_location, " out of bounds. Max: ", + -Terrain3DStorage::REGION_MAP_SIZE / 2, " to ", Terrain3DStorage::REGION_MAP_SIZE / 2 - 1); + return; + } + LOG(INFO, "Set location: ", p_location); + _location = p_location; +} + +void Terrain3DRegion::set_data(const Dictionary &p_data) { +#define SET_IF_HAS(var, str) \ + if (p_data.has(str)) { \ + var = p_data[str]; \ + } + SET_IF_HAS(_location, "location"); + SET_IF_HAS(_deleted, "deleted"); + SET_IF_HAS(_edited, "edited"); + SET_IF_HAS(_modified, "modified"); + SET_IF_HAS(_version, "version"); + SET_IF_HAS(_region_size, "region_size"); + SET_IF_HAS(_height_range, "height_range"); + SET_IF_HAS(_height_map, "height_map"); + SET_IF_HAS(_control_map, "control_map"); + SET_IF_HAS(_color_map, "color_map"); + SET_IF_HAS(_multimeshes, "multimeshes"); +} + +Dictionary Terrain3DRegion::get_data() const { + Dictionary dict; + dict["location"] = _location; + dict["deleted"] = _deleted; + dict["edited"] = _edited; + dict["modified"] = _modified; + dict["instance_id"] = String::num_uint64(get_instance_id()); // don't commit + dict["version"] = _version; + dict["region_size"] = _region_size; + dict["height_range"] = _height_range; + dict["height_map"] = _height_map; + dict["control_map"] = _control_map; + dict["color_map"] = _color_map; + dict["multimeshes"] = _multimeshes; + return dict; +} + +Ref Terrain3DRegion::duplicate(const bool p_deep) { + Ref region; + region.instantiate(); + if (!p_deep) { + region->set_data(get_data()); + } else { + Dictionary dict; + // Native type copies + dict["version"] = _version; + dict["region_size"] = _region_size; + dict["height_range"] = _height_range; + dict["modified"] = _modified; + dict["deleted"] = _deleted; + dict["location"] = _location; + // Resource duplicates + dict["height_map"] = _height_map->duplicate(); + dict["control_map"] = _control_map->duplicate(); + dict["color_map"] = _color_map->duplicate(); + Dictionary mms; + Array keys = _multimeshes.keys(); + for (int i = 0; i < keys.size(); i++) { + int mesh_id = keys[i]; + Ref mm = _multimeshes[mesh_id]; + mm->duplicate(); + mms[mesh_id] = mm; + } + dict["multimeshes"] = mms; + region->set_data(dict); + } + return region; +} + +///////////////////// +// Protected Functions +///////////////////// + +void Terrain3DRegion::_bind_methods() { + BIND_ENUM_CONSTANT(TYPE_HEIGHT); + BIND_ENUM_CONSTANT(TYPE_CONTROL); + BIND_ENUM_CONSTANT(TYPE_COLOR); + BIND_ENUM_CONSTANT(TYPE_MAX); + + ClassDB::bind_method(D_METHOD("set_version"), &Terrain3DRegion::set_version); + ClassDB::bind_method(D_METHOD("get_version"), &Terrain3DRegion::get_version); + ClassDB::bind_method(D_METHOD("set_region_size", "region_size"), &Terrain3DRegion::set_region_size); + ClassDB::bind_method(D_METHOD("get_region_size"), &Terrain3DRegion::get_region_size); + + ClassDB::bind_method(D_METHOD("set_height_map", "map"), &Terrain3DRegion::set_height_map); + ClassDB::bind_method(D_METHOD("get_height_map"), &Terrain3DRegion::get_height_map); + ClassDB::bind_method(D_METHOD("set_control_map", "map"), &Terrain3DRegion::set_control_map); + ClassDB::bind_method(D_METHOD("get_control_map"), &Terrain3DRegion::get_control_map); + ClassDB::bind_method(D_METHOD("set_color_map", "map"), &Terrain3DRegion::set_color_map); + ClassDB::bind_method(D_METHOD("get_color_map"), &Terrain3DRegion::get_color_map); + + ClassDB::bind_method(D_METHOD("set_height_range", "range"), &Terrain3DRegion::set_height_range); + ClassDB::bind_method(D_METHOD("get_height_range"), &Terrain3DRegion::get_height_range); + ClassDB::bind_method(D_METHOD("update_height", "height"), &Terrain3DRegion::update_height); + ClassDB::bind_method(D_METHOD("update_heights", "low_high"), &Terrain3DRegion::update_heights); + ClassDB::bind_method(D_METHOD("calc_height_range"), &Terrain3DRegion::calc_height_range); + + ClassDB::bind_method(D_METHOD("set_multimeshes", "multimeshes"), &Terrain3DRegion::set_multimeshes); + ClassDB::bind_method(D_METHOD("get_multimeshes"), &Terrain3DRegion::get_multimeshes); + + ClassDB::bind_method(D_METHOD("save", "path", "16-bit"), &Terrain3DRegion::save, DEFVAL(""), DEFVAL(false)); + + ClassDB::bind_method(D_METHOD("set_deleted"), &Terrain3DRegion::set_deleted); + ClassDB::bind_method(D_METHOD("is_deleted"), &Terrain3DRegion::is_deleted); + ClassDB::bind_method(D_METHOD("set_edited"), &Terrain3DRegion::set_edited); + ClassDB::bind_method(D_METHOD("is_edited"), &Terrain3DRegion::is_edited); + ClassDB::bind_method(D_METHOD("set_modified"), &Terrain3DRegion::set_modified); + ClassDB::bind_method(D_METHOD("is_modified"), &Terrain3DRegion::is_modified); + ClassDB::bind_method(D_METHOD("set_location"), &Terrain3DRegion::set_location); + ClassDB::bind_method(D_METHOD("get_location"), &Terrain3DRegion::get_location); + + ClassDB::bind_method(D_METHOD("set_data"), &Terrain3DRegion::set_data); + ClassDB::bind_method(D_METHOD("get_data"), &Terrain3DRegion::get_data); + ClassDB::bind_method(D_METHOD("duplicate", "deep"), &Terrain3DRegion::duplicate, DEFVAL(false)); + + int ro_flags = PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_READ_ONLY; + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "version", PROPERTY_HINT_NONE, "", ro_flags), "set_version", "get_version"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "region_size", PROPERTY_HINT_NONE, "", ro_flags), "set_region_size", "get_region_size"); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "height_range", PROPERTY_HINT_NONE, "", ro_flags), "set_height_range", "get_height_range"); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "heightmap", PROPERTY_HINT_RESOURCE_TYPE, "Image", ro_flags), "set_height_map", "get_height_map"); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "controlmap", PROPERTY_HINT_RESOURCE_TYPE, "Image", ro_flags), "set_control_map", "get_control_map"); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "colormap", PROPERTY_HINT_RESOURCE_TYPE, "Image", ro_flags), "set_color_map", "get_color_map"); + ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "multimeshes", PROPERTY_HINT_NONE, "", ro_flags), "set_multimeshes", "get_multimeshes"); + + // Double-clicking a region .res file shows what's on disk, the defaults, not in memory. So these are hidden + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "edited", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_edited", "is_edited"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "deleted", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_deleted", "is_deleted"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "modified", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_modified", "is_modified"); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR2I, "location", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_location", "get_location"); +} diff --git a/src/terrain_3d_region.h b/src/terrain_3d_region.h new file mode 100644 index 00000000..bd92476f --- /dev/null +++ b/src/terrain_3d_region.h @@ -0,0 +1,151 @@ +// Copyright © 2024 Cory Petkovsek, Roope Palmroos, and Contributors. + +#ifndef TERRAIN3D_REGION_CLASS_H +#define TERRAIN3D_REGION_CLASS_H + +#include "constants.h" +#include "terrain_3d_util.h" + +using namespace godot; + +class Terrain3DRegion : public Resource { + GDCLASS(Terrain3DRegion, Resource); + CLASS_NAME(); + +public: // Constants + enum MapType { + TYPE_HEIGHT, + TYPE_CONTROL, + TYPE_COLOR, + TYPE_MAX, + }; + + static inline const Image::Format FORMAT[] = { + Image::FORMAT_RF, // TYPE_HEIGHT + Image::FORMAT_RF, // TYPE_CONTROL + Image::FORMAT_RGBA8, // TYPE_COLOR + Image::Format(TYPE_MAX), // Proper size of array instead of FORMAT_MAX + }; + + static inline const char *TYPESTR[] = { + "TYPE_HEIGHT", + "TYPE_CONTROL", + "TYPE_COLOR", + "TYPE_MAX", + }; + + static inline const Color COLOR[] = { + COLOR_BLACK, // TYPE_HEIGHT + COLOR_CONTROL, // TYPE_CONTROL + COLOR_ROUGHNESS, // TYPE_COLOR + COLOR_NAN, // TYPE_MAX, unused just in case someone indexes the array + }; + +private: + /// Saved data + real_t _version = 0.8f; // Set to first version to ensure Godot always upgrades this + int _region_size = 0; + Vector2 _height_range = V2_ZERO; + // Maps + Ref _height_map; + Ref _control_map; + Ref _color_map; + // Instancer + Dictionary _multimeshes; // Dictionary[mesh_id:int] -> MultiMesh + + // Working data not saved to disk + bool _deleted = false; // Marked for deletion on save + bool _edited = false; // Marked for undo/redo storage + bool _modified = false; // Marked for saving + Vector2i _location = V2I_MAX; + +public: + Terrain3DRegion() {} + ~Terrain3DRegion() {} + + void set_version(const real_t p_version); + real_t get_version() const { return _version; } + + // Maps + void set_map(const MapType p_map_type, const Ref &p_image); + Ref get_map(const MapType p_map_type) const; + void set_maps(const TypedArray &p_maps); + TypedArray get_maps() const; + void set_height_map(const Ref &p_map); + Ref get_height_map() const { return _height_map; } + void set_control_map(const Ref &p_map); + Ref get_control_map() const { return _control_map; } + void set_color_map(const Ref &p_map); + Ref get_color_map() const { return _color_map; } + bool validate_map_size(const Ref &p_map) const; + void sanitize_maps(); + Ref sanitize_map(const MapType p_map_type, const Ref &p_map) const; + + void set_height_range(const Vector2 &p_range); + Vector2 get_height_range() const { return _height_range; } + void update_height(const real_t p_height); + void update_heights(const Vector2 &p_low_high); + void calc_height_range(); + + // Instancer + void set_multimeshes(const Dictionary &p_multimeshes) { _multimeshes = p_multimeshes; } + Dictionary get_multimeshes() const { return _multimeshes; } + + // File I/O + Error save(const String &p_path = "", const bool p_16_bit = false); + + // Working Data + void set_deleted(const bool p_deleted) { _deleted = p_deleted; } + bool is_deleted() const { return _deleted; } + void set_edited(const bool p_edited) { _edited = p_edited; } + bool is_edited() const { return _edited; } + void set_modified(const bool p_modified) { _modified = p_modified; } + bool is_modified() const { return _modified; } + void set_location(const Vector2i &p_location); + Vector2i get_location() const { return _location; } + void set_region_size(const int p_region_size) { _region_size = CLAMP(p_region_size, 1024, 1024); } + int get_region_size() const { return _region_size; } + + // Utility + void set_data(const Dictionary &p_data); + Dictionary get_data() const; + Ref duplicate(const bool p_deep = false); + +protected: + static void _bind_methods(); +}; + +typedef Terrain3DRegion::MapType MapType; +VARIANT_ENUM_CAST(Terrain3DRegion::MapType); +constexpr Terrain3DRegion::MapType TYPE_HEIGHT = Terrain3DRegion::MapType::TYPE_HEIGHT; +constexpr Terrain3DRegion::MapType TYPE_CONTROL = Terrain3DRegion::MapType::TYPE_CONTROL; +constexpr Terrain3DRegion::MapType TYPE_COLOR = Terrain3DRegion::MapType::TYPE_COLOR; +constexpr Terrain3DRegion::MapType TYPE_MAX = Terrain3DRegion::MapType::TYPE_MAX; +constexpr inline const Image::Format *FORMAT = Terrain3DRegion::FORMAT; +constexpr inline const char **TYPESTR = Terrain3DRegion::TYPESTR; +constexpr inline const Color *COLOR = Terrain3DRegion::COLOR; + +/// Inline functions + +inline void Terrain3DRegion::update_height(const real_t p_height) { + if (p_height < _height_range.x) { + _height_range.x = p_height; + _modified = true; + } else if (p_height > _height_range.y) { + _height_range.y = p_height; + _modified = true; + } +} + +inline void Terrain3DRegion::update_heights(const Vector2 &p_low_high) { + if (p_low_high.x < _height_range.x) { + _height_range.x = p_low_high.x; + _modified = true; + } + if (p_low_high.y > _height_range.y) { + _height_range.y = p_low_high.y; + _modified = true; + } +} + +#endif // TERRAIN3D_REGION_CLASS_H diff --git a/src/terrain_3d_storage.cpp b/src/terrain_3d_storage.cpp index c24e80ee..57a5eb47 100644 --- a/src/terrain_3d_storage.cpp +++ b/src/terrain_3d_storage.cpp @@ -1,10 +1,11 @@ // Copyright © 2024 Cory Petkovsek, Roope Palmroos, and Contributors. +#include +#include +#include +#include #include -#include #include -#include -#include #include "logger.h" #include "terrain_3d_storage.h" @@ -17,10 +18,12 @@ void Terrain3DStorage::_clear() { LOG(INFO, "Clearing storage"); _region_map_dirty = true; _region_map.clear(); + _region_map.resize(REGION_MAP_SIZE * REGION_MAP_SIZE); + _regions.clear(); + _master_height_range = V2_ZERO; _generated_height_maps.clear(); _generated_control_maps.clear(); _generated_color_maps.clear(); - set_multimeshes(Dictionary()); } /////////////////////////// @@ -28,76 +31,53 @@ void Terrain3DStorage::_clear() { /////////////////////////// void Terrain3DStorage::initialize(Terrain3D *p_terrain) { - if (p_terrain != nullptr) { - _terrain = p_terrain; - } else { + if (p_terrain == nullptr) { LOG(ERROR, "Initialization failed, p_terrain is null"); return; } LOG(INFO, "Initializing storage"); + bool initialized = _terrain != nullptr; + _terrain = p_terrain; _region_map.resize(REGION_MAP_SIZE * REGION_MAP_SIZE); - update_maps(); // generate map arrays -} - -Terrain3DStorage::~Terrain3DStorage() { - _clear(); -} - -// Lots of the upgrade process requires this to run first -// It only runs if the version is saved in the file, which only happens if it was -// different from the in the file is different from _version -void Terrain3DStorage::set_version(const real_t p_version) { - LOG(INFO, vformat("%.3f", p_version)); - _version = p_version; - if (_version < CURRENT_VERSION) { - LOG(WARN, "Storage version ", vformat("%.3f", _version), " will be updated to ", vformat("%.3f", CURRENT_VERSION), " upon save"); - _modified = true; - } -} - -void Terrain3DStorage::set_save_16_bit(const bool p_enabled) { - LOG(INFO, p_enabled); - _save_16_bit = p_enabled; -} - -void Terrain3DStorage::set_height_range(const Vector2 &p_range) { - LOG(INFO, vformat("%.2v", p_range)); - _height_range = p_range; -} + _mesh_vertex_spacing = _terrain->get_mesh_vertex_spacing(); + _region_size = _terrain->get_region_size(); + _region_sizev = Vector2i(_region_size, _region_size); -void Terrain3DStorage::update_heights(const real_t p_height) { - if (p_height < _height_range.x) { - _height_range.x = p_height; - _modified = true; - } else if (p_height > _height_range.y) { - _height_range.y = p_height; - _modified = true; - } - if (_modified) { - LOG(DEBUG_CONT, "Expanded height range: ", _height_range); + if (!initialized && !_terrain->get_storage_directory().is_empty()) { + load_directory(_terrain->get_storage_directory()); } } -void Terrain3DStorage::update_heights(const Vector2 &p_heights) { - if (p_heights.x < _height_range.x) { - _height_range.x = p_heights.x; - _modified = true; - } - if (p_heights.y > _height_range.y) { - _height_range.y = p_heights.y; - _modified = true; - } - if (_modified) { - LOG(DEBUG_CONT, "Expanded height range: ", _height_range); +// Returns an array of active regions, optionally a shallow or deep copy +TypedArray Terrain3DStorage::get_regions_active(const bool p_copy, const bool p_deep) const { + TypedArray region_arr; + for (int i = 0; i < _region_locations.size(); i++) { + Vector2i region_loc = _region_locations[i]; + Ref region = _regions[region_loc]; + if (region.is_valid()) { + region_arr.push_back((p_copy) ? region->duplicate(p_deep) : region); + } } + return region_arr; } -void Terrain3DStorage::update_height_range() { - _height_range = Vector2(0.f, 0.f); - for (int i = 0; i < _height_maps.size(); i++) { - update_heights(Util::get_min_max(_height_maps[i])); +// Recalculates master height range from all active regions current height ranges +// Recursive mode has all regions to recalculate from each heightmap pixel +void Terrain3DStorage::calc_height_range(const bool p_recursive) { + _master_height_range = V2_ZERO; + for (int i = 0; i < _region_locations.size(); i++) { + Vector2i region_loc = _region_locations[i]; + Ref region = _regions[region_loc]; + if (region.is_null()) { + LOG(ERROR, "Region not found at: ", region_loc); + return; + } + if (p_recursive) { + region->calc_height_range(); + } + update_master_heights(region->get_height_range()); } - LOG(INFO, "Recalculated terrain height range: ", _height_range); + LOG(DEBUG_CONT, "Accumulated height range for all regions: ", _master_height_range); } void Terrain3DStorage::clear_edited_area() { @@ -113,14 +93,40 @@ void Terrain3DStorage::add_edited_area(const AABB &p_area) { emit_signal("maps_edited", _edited_area); } -void Terrain3DStorage::set_region_size(const RegionSize p_size) { - LOG(INFO, p_size); - //ERR_FAIL_COND(p_size < SIZE_64); - //ERR_FAIL_COND(p_size > SIZE_2048); - ERR_FAIL_COND(p_size != SIZE_1024); - _region_size = p_size; - _region_sizev = Vector2i(_region_size, _region_size); - emit_signal("region_size_changed", _region_size); +void Terrain3DStorage::set_region_modified(const Vector2i &p_region_loc, const bool p_modified) { + Ref region = _regions[p_region_loc]; + if (region.is_null()) { + LOG(ERROR, "Region not found at: ", p_region_loc); + return; + } + return region->set_modified(p_modified); +} + +bool Terrain3DStorage::is_region_modified(const Vector2i &p_region_loc) const { + Ref region = _regions[p_region_loc]; + if (region.is_null()) { + LOG(ERROR, "Region not found at: ", p_region_loc); + return false; + } + return region->is_modified(); +} + +void Terrain3DStorage::set_region_deleted(const Vector2i &p_region_loc, const bool p_deleted) { + Ref region = _regions[p_region_loc]; + if (region.is_null()) { + LOG(ERROR, "Region not found at: ", p_region_loc); + return; + } + return region->set_deleted(p_deleted); +} + +bool Terrain3DStorage::is_region_deleted(const Vector2i &p_region_loc) const { + Ref region = _regions[p_region_loc]; + if (region.is_null()) { + LOG(ERROR, "Region not found at: ", p_region_loc); + return true; + } + return region->is_deleted(); } void Terrain3DStorage::set_region_locations(const TypedArray &p_locations) { @@ -130,274 +136,226 @@ void Terrain3DStorage::set_region_locations(const TypedArray &p_locati update_maps(); } -/** Returns a region location given a global position*/ -Vector2i Terrain3DStorage::get_region_location(const Vector3 &p_global_position) const { - IS_INIT_MESG("Storage not initialized", Vector2i()); - Vector3 descaled_position = p_global_position / _terrain->get_mesh_vertex_spacing(); - return Vector2i((Vector2(descaled_position.x, descaled_position.z) / real_t(_region_size)).floor()); +Error Terrain3DStorage::add_regionl(const Vector2i &p_region_loc, const Ref &p_region, const bool p_update) { + p_region->set_location(p_region_loc); + return add_region(p_region, p_update); } -// Returns Vector2i(2147483647) if out of range -Vector2i Terrain3DStorage::get_region_location_from_id(const int p_region_id) const { - if (p_region_id < 0 || p_region_id >= _region_locations.size()) { - return Vector2i(INT32_MAX, INT32_MAX); - } - return _region_locations[p_region_id]; +Error Terrain3DStorage::add_regionp(const Vector3 &p_global_position, const Ref &p_region, const bool p_update) { + p_region->set_location(get_region_location(p_global_position)); + return add_region(p_region, p_update); } -int Terrain3DStorage::get_region_id(const Vector3 &p_global_position) const { - Vector2i region_loc = get_region_location(p_global_position); - return get_region_id_from_location(region_loc); +Ref Terrain3DStorage::add_region_blank(const Vector2i &p_region_loc, const bool p_update) { + Ref region; + region.instantiate(); + region->set_location(p_region_loc); + region->set_region_size(_region_size); + if (add_region(region, p_update) == OK) { + region->set_modified(true); + return region; + } + return Ref(); } -// Returns -1 if out of bounds, 0 if no region, or region id -int Terrain3DStorage::get_region_id_from_location(const Vector2i &p_region_loc) const { - int map_index = _get_region_map_index(p_region_loc); - if (map_index >= 0) { - int region_id = _region_map[map_index] - 1; // 0 = no region - if (region_id >= 0 && region_id < _region_locations.size()) { - return region_id; - } - } - return -1; +Ref Terrain3DStorage::add_region_blankp(const Vector3 &p_global_position, const bool p_update) { + return add_region_blank(get_region_location(p_global_position)); } -/** Adds a region to the terrain - * Option to include an array of Images to use for maps - * Map types are Height:0, Control:1, Color:2, defined in MapType - * If the region already exists and maps are included, the current maps will be overwritten - * Parameters: - * p_global_position - the world location to place the region, rounded down to the nearest region_size multiple - * p_images - Optional array of [ Height, Control, Color ... ] w/ region_sized images +/** Adds a Terrain3DRegion to the terrain + * Marks region as modified * p_update - rebuild the maps if true. Set to false if bulk adding many regions. */ -Error Terrain3DStorage::add_region(const Vector3 &p_global_position, const TypedArray &p_images, const bool p_update) { - IS_INIT_MESG("Storage not initialized", FAILED); - Vector2i region_loc = get_region_location(p_global_position); - LOG(INFO, "Adding region at ", p_global_position, ", region_loc ", region_loc, - ", array size: ", p_images.size(), - ", update maps: ", p_update ? "yes" : "no"); - - if (_get_region_map_index(region_loc) < 0) { - uint64_t time = Time::get_singleton()->get_ticks_msec(); - if (time - _last_region_bounds_error > 1000) { - _last_region_bounds_error = time; - LOG(ERROR, "Specified position outside of maximum region map size: +/-", - real_t((REGION_MAP_SIZE / 2) * _region_size) * _terrain->get_mesh_vertex_spacing()); - } +Error Terrain3DStorage::add_region(const Ref &p_region, const bool p_update) { + if (p_region.is_null()) { + LOG(ERROR, "Provided region is null. Returning"); return FAILED; } + Vector2i region_loc = p_region->get_location(); + LOG(INFO, "Adding region at location ", region_loc, ", update maps: ", p_update ? "yes" : "no"); - if (has_region(p_global_position)) { - if (p_images.is_empty()) { - LOG(DEBUG, "Region at ", p_global_position, " already exists and nothing to overwrite. Doing nothing"); - return OK; - } else { - LOG(DEBUG, "Region at ", p_global_position, " already exists, overwriting"); - remove_region(p_global_position, false); - } - } - - TypedArray images = sanitize_maps(TYPE_MAX, p_images); - if (images.is_empty()) { - LOG(ERROR, "Sanitize_maps failed to accept images or produce blanks"); + // Check bounds and slow report errors + if (get_region_map_index(region_loc) < 0) { + LOG(ERROR, "Location ", region_loc, " out of bounds. Max: ", + -REGION_MAP_SIZE / 2, " to ", REGION_MAP_SIZE / 2 - 1); return FAILED; } - - // If we're importing data into a region, check its heights for aabbs - Vector2 min_max = Vector2(0.f, 0.f); - if (p_images.size() > TYPE_HEIGHT) { - min_max = Util::get_min_max(images[TYPE_HEIGHT]); - LOG(DEBUG, "Checking imported height range: ", min_max); - update_heights(min_max); + p_region->sanitize_maps(); + p_region->set_deleted(false); + if (!_region_locations.has(region_loc)) { + _region_locations.push_back(region_loc); + } else { + LOG(INFO, "Overwriting ", (_regions.has(region_loc)) ? "deleted" : "existing", " region at ", region_loc); } - - LOG(DEBUG, "Pushing back ", images.size(), " images"); - _height_maps.push_back(images[TYPE_HEIGHT]); - _control_maps.push_back(images[TYPE_CONTROL]); - _color_maps.push_back(images[TYPE_COLOR]); - _region_locations.push_back(region_loc); - LOG(DEBUG, "Total regions after pushback: ", _region_locations.size()); - - // Region_map is used by get_region_id so must be updated every time + _regions[region_loc] = p_region; _region_map_dirty = true; + LOG(DEBUG, "Storing region ", region_loc, " version ", vformat("%.3f", p_region->get_version()), " id: ", _region_locations.size()); if (p_update) { - LOG(DEBUG, "Updating generated maps"); - _generated_height_maps.clear(); - _generated_control_maps.clear(); - _generated_color_maps.clear(); - update_maps(); - notify_property_list_changed(); - emit_changed(); - } else { - update_maps(); + force_update_maps(); } return OK; } -void Terrain3DStorage::remove_region(const Vector3 &p_global_position, const bool p_update) { - LOG(INFO, "Removing region at ", p_global_position, " Updating: ", p_update ? "yes" : "no"); - int region_id = get_region_id(p_global_position); - ERR_FAIL_COND_MSG(region_id == -1, "Map does not exist."); +void Terrain3DStorage::remove_regionp(const Vector3 &p_global_position, const bool p_update) { + Ref region = get_region(get_region_location(p_global_position)); + remove_region(region, p_update); +} - LOG(INFO, "Removing region at: ", get_region_location(p_global_position)); - _region_locations.remove_at(region_id); - LOG(DEBUG, "Removed region_locations, new size: ", _region_locations.size()); - _height_maps.remove_at(region_id); - LOG(DEBUG, "Removed heightmaps, new size: ", _height_maps.size()); - _control_maps.remove_at(region_id); - LOG(DEBUG, "Removed control maps, new size: ", _control_maps.size()); - _color_maps.remove_at(region_id); - LOG(DEBUG, "Removed colormaps, new size: ", _color_maps.size()); +void Terrain3DStorage::remove_regionl(const Vector2i &p_region_loc, const bool p_update) { + Ref region = get_region(p_region_loc); + remove_region(region, p_update); +} - if (_height_maps.size() == 0) { - _height_range = Vector2(0.f, 0.f); +// Remove region marks the region for deletion, and removes it from the active arrays indexed by ID +// It remains stored in _regions and the file remains on disk until saved, when both are removed +void Terrain3DStorage::remove_region(const Ref &p_region, const bool p_update) { + if (p_region.is_null()) { + LOG(ERROR, "Region not found or is null. Returning"); + return; } - // Region_map is used by get_region_id so must be updated + Vector2i region_loc = p_region->get_location(); + int region_id = _region_locations.find(region_loc); + LOG(INFO, "Marking region ", region_loc, " for deletion. update_maps: ", p_update ? "yes" : "no"); + if (region_id < 0) { + LOG(ERROR, "Region ", region_loc, " not found in region_locations. Returning"); + return; + } + p_region->set_deleted(true); + _region_locations.remove_at(region_id); _region_map_dirty = true; + LOG(DEBUG, "Removing from region_locations, new size: ", _region_locations.size()); if (p_update) { LOG(DEBUG, "Updating generated maps"); - _generated_height_maps.clear(); - _generated_control_maps.clear(); - _generated_color_maps.clear(); - update_maps(); - notify_property_list_changed(); - emit_changed(); - } else { - update_maps(); + force_update_maps(); } } void Terrain3DStorage::update_maps() { bool any_changed = false; + + if (_region_map_dirty) { + LOG(DEBUG_CONT, "Regenerating ", REGION_MAP_VSIZE, " region map array from active regions"); + _region_map.clear(); + _region_map.resize(REGION_MAP_SIZE * REGION_MAP_SIZE); + _region_map_dirty = false; + _region_locations = TypedArray(); // enforce new pointer + Array locs = _regions.keys(); + int region_id = 0; + for (int i = 0; i < locs.size(); i++) { + Ref region = _regions[locs[i]]; + if (region.is_valid() && !region->is_deleted()) { + region_id += 1; // Begin at 1 since 0 = no region + int map_index = get_region_map_index(region->get_location()); + if (map_index >= 0) { + _region_map[map_index] = region_id; + _region_locations.push_back(region->get_location()); + } + } + } + any_changed = true; + emit_signal("region_map_changed"); + } + if (_generated_height_maps.is_dirty()) { - LOG(DEBUG_CONT, "Regenerating height layered texture from ", _height_maps.size(), " maps"); + LOG(DEBUG_CONT, "Regenerating height texture array from regions"); + _height_maps.clear(); + for (int i = 0; i < _region_locations.size(); i++) { + Vector2i region_loc = _region_locations[i]; + Ref region = _regions[region_loc]; + if (region.is_valid()) { + _height_maps.push_back(region->get_height_map()); + } else { + LOG(ERROR, "Can't find region ", region_loc, ", _regions: ", _regions.size(), + ", locations: ", _region_locations.size(), ". Please report this error."); + _region_map_dirty = true; + } + } _generated_height_maps.create(_height_maps); - _modified = true; + calc_height_range(); any_changed = true; emit_signal("height_maps_changed"); } if (_generated_control_maps.is_dirty()) { - LOG(DEBUG_CONT, "Regenerating control layered texture from ", _control_maps.size(), " maps"); + LOG(DEBUG_CONT, "Regenerating control texture array from regions"); + _control_maps.clear(); + for (int i = 0; i < _region_locations.size(); i++) { + Vector2i region_loc = _region_locations[i]; + Ref region = _regions[region_loc]; + _control_maps.push_back(region->get_control_map()); + } _generated_control_maps.create(_control_maps); - _modified = true; any_changed = true; emit_signal("control_maps_changed"); } if (_generated_color_maps.is_dirty()) { - LOG(DEBUG_CONT, "Regenerating color layered texture from ", _color_maps.size(), " maps"); - for (int i = 0; i < _color_maps.size(); i++) { - Ref map = _color_maps[i]; - map->generate_mipmaps(); + LOG(DEBUG_CONT, "Regenerating color texture array from regions"); + _color_maps.clear(); + for (int i = 0; i < _region_locations.size(); i++) { + Vector2i region_loc = _region_locations[i]; + Ref region = _regions[region_loc]; + _color_maps.push_back(region->get_color_map()); } _generated_color_maps.create(_color_maps); - _modified = true; any_changed = true; emit_signal("color_maps_changed"); } - if (_region_map_dirty) { - LOG(DEBUG_CONT, "Regenerating ", REGION_MAP_VSIZE, " region map array"); - _region_map.clear(); - _region_map.resize(REGION_MAP_SIZE * REGION_MAP_SIZE); - _region_map_dirty = false; - for (int i = 0; i < _region_locations.size(); i++) { - int map_index = _get_region_map_index(_region_locations[i]); - if (map_index >= 0) { - _region_map[map_index] = i + 1; // Begin at 1 since 0 = no region - } - } - _modified = true; - any_changed = true; - emit_signal("region_map_changed"); - } if (any_changed) { emit_signal("maps_changed"); } } -void Terrain3DStorage::set_map_region(const MapType p_map_type, const int p_region_id, const Ref &p_image) { - switch (p_map_type) { - case TYPE_HEIGHT: - if (p_region_id >= 0 && p_region_id < _height_maps.size()) { - _height_maps[p_region_id] = p_image; - force_update_maps(TYPE_HEIGHT); - } else { - LOG(ERROR, "Requested region id is out of bounds. height_maps size: ", _height_maps.size()); - } - break; - case TYPE_CONTROL: - if (p_region_id >= 0 && p_region_id < _control_maps.size()) { - _control_maps[p_region_id] = p_image; - force_update_maps(TYPE_CONTROL); - } else { - LOG(ERROR, "Requested region id is out of bounds. control_maps size: ", _control_maps.size()); - } - break; - case TYPE_COLOR: - if (p_region_id >= 0 && p_region_id < _color_maps.size()) { - _color_maps[p_region_id] = p_image; - force_update_maps(TYPE_COLOR); - } else { - LOG(ERROR, "Requested region id is out of bounds. color_maps size: ", _color_maps.size()); - } - break; - default: - LOG(ERROR, "Requested map type is invalid"); - break; +void Terrain3DStorage::save_region(const Vector2i &p_region_loc, const String &p_dir, const bool p_16_bit) { + Ref region = _regions[p_region_loc]; + if (region.is_null()) { + LOG(ERROR, "No region found at: ", p_region_loc); + return; } -} - -Ref Terrain3DStorage::get_map_region(const MapType p_map_type, const int p_region_id) const { - switch (p_map_type) { - case TYPE_HEIGHT: - if (p_region_id >= 0 && p_region_id < _height_maps.size()) { - return _height_maps[p_region_id]; - } else { - LOG(ERROR, "Requested region id is out of bounds. height_maps size: ", _height_maps.size()); - } - break; - case TYPE_CONTROL: - if (p_region_id >= 0 && p_region_id < _control_maps.size()) { - return _control_maps[p_region_id]; - } else { - LOG(ERROR, "Requested region id is out of bounds. control_maps size: ", _control_maps.size()); - } - break; - case TYPE_COLOR: - if (p_region_id >= 0 && p_region_id < _color_maps.size()) { - return _color_maps[p_region_id]; - } else { - LOG(ERROR, "Requested region id is out of bounds. color_maps size: ", _color_maps.size()); - } - break; - default: - LOG(ERROR, "Requested map type is invalid"); - break; + String fname = Util::location_to_filename(p_region_loc); + String path = p_dir + String("/") + fname; + // If region marked for deletion, remove from disk and from _regions, but don't free in case stored in undo + if (region->is_deleted()) { + LOG(DEBUG, "Removing ", p_region_loc, " from _regions"); + _regions.erase(p_region_loc); + LOG(DEBUG, "File to be deleted: ", path); + if (!FileAccess::file_exists(path)) { + LOG(INFO, "File to delete ", path, " doesn't exist. (Maybe from add, undo, save)"); + return; + } + Ref da = DirAccess::open(p_dir); + if (da.is_null()) { + LOG(ERROR, "Cannot open directory for writing: ", p_dir, " error: ", DirAccess::get_open_error()); + return; + } + da->remove(fname); + if (Engine::get_singleton()->is_editor_hint()) { + EditorInterface::get_singleton()->get_resource_filesystem()->scan(); + } + LOG(INFO, "File ", path, " deleted"); + return; } - return Ref(); + region->save(path, p_16_bit); } -void Terrain3DStorage::set_maps(const MapType p_map_type, const TypedArray &p_maps) { - ERR_FAIL_COND_MSG(p_map_type < 0 || p_map_type >= TYPE_MAX, "Specified map type out of range"); - LOG(INFO, "Setting ", TYPESTR[p_map_type], " maps: ", p_maps.size()); - switch (p_map_type) { - case TYPE_HEIGHT: - _height_maps = sanitize_maps(TYPE_HEIGHT, p_maps); - break; - case TYPE_CONTROL: - _control_maps = sanitize_maps(TYPE_CONTROL, p_maps); - break; - case TYPE_COLOR: - _color_maps = sanitize_maps(TYPE_COLOR, p_maps); - break; - default: - break; +void Terrain3DStorage::load_region(const Vector2i &p_region_loc, const String &p_dir, const bool p_update) { + LOG(INFO, "Loading region from location ", p_region_loc); + String path = p_dir + String("/") + Util::location_to_filename(p_region_loc); + if (!FileAccess::file_exists(path)) { + LOG(ERROR, "File ", path, " doesn't exist"); + return; } - force_update_maps(p_map_type); + Ref region = ResourceLoader::get_singleton()->load(path, "Terrain3DRegion", ResourceLoader::CACHE_MODE_IGNORE); + if (region.is_null()) { + LOG(ERROR, "Cannot load region at ", path); + return; + } + region->take_over_path(path); + region->set_location(p_region_loc); + region->set_version(CURRENT_VERSION); // Sends upgrade warning if old version + add_region(region, p_update); } TypedArray Terrain3DStorage::get_maps(const MapType p_map_type) const { @@ -421,74 +379,80 @@ TypedArray Terrain3DStorage::get_maps(const MapType p_map_type) const { return TypedArray(); } -TypedArray Terrain3DStorage::get_maps_copy(const MapType p_map_type) const { +TypedArray Terrain3DStorage::get_maps_copy(const MapType p_map_type, const TypedArray &p_region_ids) const { if (p_map_type < 0 || p_map_type >= TYPE_MAX) { LOG(ERROR, "Specified map type out of range"); return TypedArray(); } TypedArray maps = get_maps(p_map_type); TypedArray newmaps; - newmaps.resize(maps.size()); - for (int i = 0; i < maps.size(); i++) { - Ref img; - img.instantiate(); - img->copy_from(maps[i]); - newmaps[i] = img; + if (p_region_ids.is_empty()) { + newmaps.resize(maps.size()); + for (int i = 0; i < maps.size(); i++) { + Ref img; + img.instantiate(); + img->copy_from(maps[i]); + newmaps[i] = img; + } + } else { + newmaps.resize(p_region_ids.size()); + for (int i = 0; i < p_region_ids.size(); i++) { + Ref img; + img.instantiate(); + img->copy_from(maps[p_region_ids[i]]); + newmaps[i] = img; + } } return newmaps; } void Terrain3DStorage::set_pixel(const MapType p_map_type, const Vector3 &p_global_position, const Color &p_pixel) { - IS_INIT_MESG("Storage not initialized", VOID); if (p_map_type < 0 || p_map_type >= TYPE_MAX) { LOG(ERROR, "Specified map type out of range"); return; } - int region_id = get_region_id(p_global_position); - if (region_id < 0) { + Vector2i region_loc = get_region_location(p_global_position); + Ref region = _regions[region_loc]; + if (region.is_null()) { + LOG(ERROR, "No region found at: ", p_global_position); return; } - Vector2i region_loc = _region_locations[region_id]; Vector2i global_offset = region_loc * _region_size; - Vector3 descaled_pos = p_global_position / _terrain->get_mesh_vertex_spacing(); + Vector3 descaled_pos = p_global_position / _mesh_vertex_spacing; Vector2i img_pos = Vector2i(descaled_pos.x - global_offset.x, descaled_pos.z - global_offset.y); - img_pos = img_pos.clamp(Vector2i(), Vector2i(_region_size - 1, _region_size - 1)); - Ref map = get_map_region(p_map_type, region_id); + img_pos = img_pos.clamp(V2I_ZERO, Vector2i(_region_size - 1, _region_size - 1)); + Ref map = region->get_map(p_map_type); map->set_pixelv(img_pos, p_pixel); + region->set_modified(true); } Color Terrain3DStorage::get_pixel(const MapType p_map_type, const Vector3 &p_global_position) const { - IS_INIT_MESG("Storage not initialized", COLOR_NAN); if (p_map_type < 0 || p_map_type >= TYPE_MAX) { LOG(ERROR, "Specified map type out of range"); return COLOR_NAN; } - int region_id = get_region_id(p_global_position); - if (region_id < 0) { + Vector2i region_loc = get_region_location(p_global_position); + Ref region = _regions[region_loc]; + if (region.is_null()) { return COLOR_NAN; } - Vector2i region_loc = _region_locations[region_id]; Vector2i global_offset = region_loc * _region_size; - Vector3 descaled_pos = p_global_position / _terrain->get_mesh_vertex_spacing(); + Vector3 descaled_pos = p_global_position / _mesh_vertex_spacing; Vector2i img_pos = Vector2i(descaled_pos.x - global_offset.x, descaled_pos.z - global_offset.y); - img_pos = img_pos.clamp(Vector2i(), Vector2i(_region_size - 1, _region_size - 1)); - Ref map = get_map_region(p_map_type, region_id); + img_pos = img_pos.clamp(V2I_ZERO, Vector2i(_region_size - 1, _region_size - 1)); + Ref map = region->get_map(p_map_type); return map->get_pixelv(img_pos); } real_t Terrain3DStorage::get_height(const Vector3 &p_global_position) const { - IS_INIT_MESG("Storage not initialized", NAN); if (is_hole(get_control(p_global_position))) { return NAN; } Vector3 pos = p_global_position; - real_t step = _terrain->get_mesh_vertex_spacing(); + const real_t &step = _mesh_vertex_spacing; pos.y = 0.f; // Round to nearest vertex - Vector3 pos_round = Vector3( - round_multiple(pos.x, step), - 0.f, - round_multiple(pos.z, step)); + Vector3 pos_round = Vector3(round_multiple(pos.x, step), 0.f, round_multiple(pos.z, step)); // If requested position is close to a vertex, return its height if ((pos - pos_round).length() < 0.01f) { return get_pixel(TYPE_HEIGHT, pos).r; @@ -517,41 +481,43 @@ real_t Terrain3DStorage::get_height(const Vector3 &p_global_position) const { * value of .3-.5, otherwise it's the base texture. **/ Vector3 Terrain3DStorage::get_texture_id(const Vector3 &p_global_position) const { - IS_INIT_MESG("Storage not initialized", Vector3(NAN, NAN, NAN);); - int region_id = get_region_id(p_global_position); + // Verify in a region + int region_id = get_region_idp(p_global_position); if (region_id < 0) { - // Not in a region return Vector3(NAN, NAN, NAN); } + + // Verify not in a hole float src = get_pixel(TYPE_CONTROL, p_global_position).r; // 32-bit float, not double/real if (is_hole(src)) { return Vector3(NAN, NAN, NAN); } - Ref t_material = _terrain->get_material(); - bool auto_enabled = t_material->get_auto_shader(); - bool control_auto = is_auto(src); - uint32_t base_id; - uint32_t overlay_id; - real_t blend; - // Autoshader is enabled, and is enabled at the current location. - if (auto_enabled && control_auto) { - real_t auto_slope = real_t(t_material->get_shader_param("auto_slope")) * 2.f - 1.f; - real_t auto_height_reduction = real_t(t_material->get_shader_param("auto_height_reduction")); - real_t height = get_height(p_global_position); - Vector3 normal = get_normal(p_global_position); - base_id = t_material->get_shader_param("auto_base_texture"); - overlay_id = t_material->get_shader_param("auto_overlay_texture"); - blend = CLAMP( - vec3_dot(Vector3(0.f, 1.f, 0.f), - normal * auto_slope * 2.f - Vector3(auto_slope, auto_slope, auto_slope)) - - auto_height_reduction * .01f * height, - 0.f, 1.f); - // Return control map values. - } else { - base_id = get_base(src); - overlay_id = get_overlay(src); - blend = real_t(get_blend(src)) / 255.0f; + + // If material available, autoshader enabled, and pixel set to auto + if (_terrain != nullptr) { + Ref t_material = _terrain->get_material(); + bool auto_enabled = t_material->get_auto_shader(); + bool control_auto = is_auto(src); + if (auto_enabled && control_auto) { + real_t auto_slope = real_t(t_material->get_shader_param("auto_slope")) * 2.f - 1.f; + real_t auto_height_reduction = real_t(t_material->get_shader_param("auto_height_reduction")); + real_t height = get_height(p_global_position); + Vector3 normal = get_normal(p_global_position); + uint32_t base_id = t_material->get_shader_param("auto_base_texture"); + uint32_t overlay_id = t_material->get_shader_param("auto_overlay_texture"); + real_t blend = CLAMP( + vec3_dot(Vector3(0.f, 1.f, 0.f), + normal * auto_slope * 2.f - Vector3(auto_slope, auto_slope, auto_slope)) - + auto_height_reduction * .01f * height, + 0.f, 1.f); + return Vector3(real_t(base_id), real_t(overlay_id), blend); + } } + + // Else, just get textures from control map + uint32_t base_id = get_base(src); + uint32_t overlay_id = get_overlay(src); + real_t blend = real_t(get_blend(src)) / 255.0f; return Vector3(real_t(base_id), real_t(overlay_id), blend); } @@ -562,7 +528,7 @@ real_t Terrain3DStorage::get_angle(const Vector3 &p_global_position) const { } real_t angle = real_t(get_uv_rotation(src)); angle *= 22.5; // Return value in degrees. - return real_t(angle); + return angle; } real_t Terrain3DStorage::get_scale(const Vector3 &p_global_position) const { @@ -572,82 +538,11 @@ real_t Terrain3DStorage::get_scale(const Vector3 &p_global_position) const { } std::array scale_values = { 0.0f, 20.0f, 40.0f, 60.0f, 80.0f, -60.0f, -40.0f, -20.0f }; real_t scale = scale_values[get_uv_scale(src)]; //select from array UI return values - return real_t(scale); -} - -/** - * Returns sanitized maps of either a region set or a uniform set - * Verifies size, vailidity, and format of maps - * Creates filled blanks if lacking - * p_map_type: - * TYPE_HEIGHT, TYPE_CONTROL, TYPE_COLOR: uniform set - p_maps are all the same type, size=N - * TYPE_MAX = region set - p_maps is [ height, control, color ], size=3 - **/ -TypedArray Terrain3DStorage::sanitize_maps(const MapType p_map_type, const TypedArray &p_maps) const { - LOG(INFO, "Verifying image set is valid: ", p_maps.size(), " maps of type: ", TYPESTR[TYPE_MAX]); - - TypedArray images; - int iterations; - - if (p_map_type == TYPE_MAX) { - images.resize(TYPE_MAX); - iterations = TYPE_MAX; - } else { - images.resize(p_maps.size()); - iterations = p_maps.size(); - if (iterations <= 0) { - LOG(DEBUG, "Empty Image set. Nothing to sanitize"); - return images; - } - } - - Image::Format format; - const char *type_str; - Color color; - for (int i = 0; i < iterations; i++) { - if (p_map_type == TYPE_MAX) { - format = FORMAT[i]; - type_str = TYPESTR[i]; - color = COLOR[i]; - } else { - format = FORMAT[p_map_type]; - type_str = TYPESTR[p_map_type]; - color = COLOR[p_map_type]; - } - - if (i < p_maps.size()) { - Ref img; - img = p_maps[i]; - if (img.is_valid()) { - if (img->get_size() == _region_sizev) { - if (img->get_format() == format) { - LOG(DEBUG, "Map type ", type_str, " correct format, size. Using image"); - images[i] = img; - } else { - LOG(DEBUG, "Provided ", type_str, " map wrong format: ", img->get_format(), ". Converting copy to: ", format); - Ref newimg; - newimg.instantiate(); - newimg->copy_from(img); - newimg->convert(format); - images[i] = newimg; - } - continue; // Continue for loop - } else { - LOG(DEBUG, "Provided ", type_str, " map wrong size: ", img->get_size(), ". Creating blank"); - } - } else { - LOG(DEBUG, "No provided ", type_str, " map. Creating blank"); - } - } else { - LOG(DEBUG, "p_images.size() < ", i, ". Creating blank"); - } - images[i] = Util::get_filled_image(_region_sizev, color, false, format); - } - - return images; + return scale; } -void Terrain3DStorage::force_update_maps(const MapType p_map_type) { +void Terrain3DStorage::force_update_maps(const MapType p_map_type, const bool p_generate_mipmaps) { + LOG(DEBUG_CONT, "Regenerating maps of type: ", p_map_type); switch (p_map_type) { case TYPE_HEIGHT: _generated_height_maps.clear(); @@ -662,56 +557,25 @@ void Terrain3DStorage::force_update_maps(const MapType p_map_type) { _generated_height_maps.clear(); _generated_control_maps.clear(); _generated_color_maps.clear(); + _region_map_dirty = true; break; } - update_maps(); -} - -void Terrain3DStorage::set_multimeshes(const Dictionary &p_multimeshes) { - LOG(INFO, "Loading multimeshes: ", p_multimeshes); - if (_multimeshes != p_multimeshes) { - _multimeshes = p_multimeshes; - emit_signal("multimeshes_changed"); + if (p_generate_mipmaps && (p_map_type == TYPE_COLOR || p_map_type == TYPE_MAX)) { + LOG(DEBUG_CONT, "Regenerating color mipmaps"); + for (int i = 0; i < _region_locations.size(); i++) { + Vector2i region_loc = _region_locations[i]; + Ref region = _regions[region_loc]; + region->get_color_map()->generate_mipmaps(); + } } + update_maps(); } -void Terrain3DStorage::save() { - if (!_modified) { - LOG(INFO, "Save requested, but not modified. Skipping"); - return; - } - String path = get_path(); - // Initiate save to external file. The scene will save itself. - if (path.get_extension() == "tres" || path.get_extension() == "res") { - LOG(DEBUG, "Attempting to save terrain data to external file: " + path); - LOG(DEBUG, "Saving storage version: ", vformat("%.3f", CURRENT_VERSION)); - set_version(CURRENT_VERSION); - Error err; - if (_save_16_bit) { - LOG(DEBUG, "16-bit save requested, converting heightmaps"); - TypedArray original_maps; - original_maps = get_maps_copy(TYPE_HEIGHT); - for (int i = 0; i < _height_maps.size(); i++) { - Ref img = _height_maps[i]; - img->convert(Image::FORMAT_RH); - } - LOG(DEBUG, "Images converted, saving"); - err = ResourceSaver::get_singleton()->save(this, path, ResourceSaver::FLAG_COMPRESS); - LOG(DEBUG, "Restoring 32-bit maps"); - _height_maps = original_maps; - - } else { - err = ResourceSaver::get_singleton()->save(this, path, ResourceSaver::FLAG_COMPRESS); - } - ERR_FAIL_COND(err); - LOG(DEBUG, "ResourceSaver return error (0 is OK): ", err); - if (err == OK) { - _modified = false; - } - LOG(INFO, "Finished saving terrain data"); - } - if (path.get_extension() != "res") { - LOG(WARN, "Storage resource is not saved as an external, binary .res file"); +void Terrain3DStorage::save_directory(const String &p_dir) { + LOG(INFO, "Saving data files to ", p_dir); + Array locations = _regions.keys(); + for (int i = 0; i < locations.size(); i++) { + save_region(locations[i], p_dir, _terrain->get_save_16_bit()); } } @@ -731,7 +595,7 @@ void Terrain3DStorage::import_images(const TypedArray &p_images, const Ve return; } - Vector2i img_size = Vector2i(0, 0); + Vector2i img_size = V2I_ZERO; for (int i = 0; i < TYPE_MAX; i++) { Ref img = p_images[i]; if (img.is_valid() && !img->is_empty()) { @@ -739,7 +603,7 @@ void Terrain3DStorage::import_images(const TypedArray &p_images, const Ve if (i == TYPE_HEIGHT) { LOG(INFO, "Applying offset: ", p_offset, ", scale: ", p_scale); } - if (img_size == Vector2i(0, 0)) { + if (img_size == V2I_ZERO) { img_size = img->get_size(); } else if (img_size != img->get_size()) { LOG(ERROR, "Included Images in p_images have different dimensions. Aborting import"); @@ -747,22 +611,21 @@ void Terrain3DStorage::import_images(const TypedArray &p_images, const Ve } } } - if (img_size == Vector2i(0, 0)) { + if (img_size == V2I_ZERO) { LOG(ERROR, "All images are empty. Nothing to import"); return; } - real_t vertex_spacing = _terrain->get_mesh_vertex_spacing(); - Vector3 descaled_position = p_global_position / vertex_spacing; + Vector3 descaled_position = p_global_position / _mesh_vertex_spacing; int max_dimension = _region_size * REGION_MAP_SIZE / 2; if ((abs(descaled_position.x) > max_dimension) || (abs(descaled_position.z) > max_dimension)) { - LOG(ERROR, "Specify a position within +/-", Vector3(max_dimension, 0.f, max_dimension) * vertex_spacing); + LOG(ERROR, "Specify a position within +/-", Vector3(max_dimension, 0.f, max_dimension) * _mesh_vertex_spacing); return; } if ((descaled_position.x + img_size.x > max_dimension) || (descaled_position.z + img_size.y > max_dimension)) { LOG(ERROR, img_size, " image will not fit at ", p_global_position, - ". Try ", -(img_size * vertex_spacing) / 2.f, " to center"); + ". Try ", -(img_size * _mesh_vertex_spacing) / 2.f, " to center"); return; } @@ -816,26 +679,25 @@ void Terrain3DStorage::import_images(const TypedArray &p_images, const Ve LOG(DEBUG, "Copying ", size_to_copy, " sized segment"); TypedArray images; images.resize(TYPE_MAX); - Vector3 position = Vector3(descaled_position.x + start_coords.x, 0.f, descaled_position.z + start_coords.y) * vertex_spacing; - int region_id = get_region_id(position); for (int i = 0; i < TYPE_MAX; i++) { Ref img = tmp_images[i]; Ref img_slice; - // If not in a region, generate a new empty map. - if (region_id == -1) { - img_slice = Util::get_filled_image(_region_sizev, COLOR[i], false, FORMAT[i]); - // Otherwise Get the current map. - } else { - img_slice = get_map_region(MapType(i), region_id); - } if (img.is_valid() && !img->is_empty()) { - img_slice->convert(img->get_format()); - img_slice->blit_rect(tmp_images[i], Rect2i(start_coords, size_to_copy), Vector2i(0, 0)); + img_slice = Util::get_filled_image(_region_sizev, COLOR[i], false, img->get_format()); + img_slice->blit_rect(tmp_images[i], Rect2i(start_coords, size_to_copy), V2I_ZERO); + } else { + img_slice = Util::get_filled_image(_region_sizev, COLOR[i], false, FORMAT[i]); } images[i] = img_slice; } // Add the heightmap slice and only regenerate on the last one - add_region(position, images, (x == slices_width - 1 && y == slices_height - 1)); + Ref region; + region.instantiate(); + Vector3 position = Vector3(descaled_position.x + start_coords.x, 0.f, descaled_position.z + start_coords.y); + position *= _mesh_vertex_spacing; + region->set_location(get_region_location(position)); + region->set_maps(images); + add_region(region, (x == slices_width - 1 && y == slices_height - 1)); } } // for y < slices_height, x < slices_width } @@ -888,10 +750,10 @@ Error Terrain3DStorage::export_image(const String &p_file_name, const MapType p_ file_name = "res://" + file_name; } - // Check if the file could be opened for writing + // Check if the file can be opened for writing Ref file_ref = FileAccess::open(file_name, FileAccess::ModeFlags::WRITE); if (file_ref.is_null()) { - LOG(ERROR, "Could not open file '" + file_name + "' for writing"); + LOG(ERROR, "Cannot open file '" + file_name + "' for writing"); return FAILED; } file_ref->close(); @@ -899,7 +761,7 @@ Error Terrain3DStorage::export_image(const String &p_file_name, const MapType p_ // Filename is validated. Begin export image generation Ref img = layered_to_image(p_map_type); if (img.is_null() || img->is_empty()) { - LOG(ERROR, "Could not create an export image for map type: ", TYPESTR[p_map_type]); + LOG(ERROR, "Cannot create an export image for map type: ", TYPESTR[p_map_type]); return FAILED; } @@ -942,8 +804,8 @@ Ref Terrain3DStorage::layered_to_image(const MapType p_map_type) const { if (map_type >= TYPE_MAX) { map_type = TYPE_HEIGHT; } - Vector2i top_left = Vector2i(0, 0); - Vector2i bottom_right = Vector2i(0, 0); + Vector2i top_left = V2I_ZERO; + Vector2i bottom_right = V2I_ZERO; for (int i = 0; i < _region_locations.size(); i++) { LOG(DEBUG, "Region locations[", i, "]: ", _region_locations[i]); Vector2i region_loc = _region_locations[i]; @@ -968,12 +830,51 @@ Ref Terrain3DStorage::layered_to_image(const MapType p_map_type) const { Vector2i region_loc = _region_locations[i]; Vector2i img_location = (region_loc - top_left) * _region_size; LOG(DEBUG, "Region to blit: ", region_loc, " Export image coords: ", img_location); - int region_id = get_region_id(Vector3(region_loc.x, 0, region_loc.y) * _region_size); - img->blit_rect(get_map_region(map_type, region_id), Rect2i(Vector2i(0, 0), _region_sizev), img_location); + Ref region = _regions[region_loc]; + img->blit_rect(region->get_map(map_type), Rect2i(V2I_ZERO, _region_sizev), img_location); } return img; } +void Terrain3DStorage::load_directory(const String &p_dir) { + if (p_dir.is_empty()) { + LOG(ERROR, "Specified data directory is blank"); + return; + } + Ref da = DirAccess::open(p_dir); + if (da.is_null()) { + LOG(ERROR, "Cannot read Terrain3D data directory: ", p_dir); + return; + } + _clear(); + + LOG(INFO, "Loading region files from ", p_dir); + PackedStringArray files = da->get_files(); + for (int i = 0; i < files.size(); i++) { + String fname = files[i]; + String path = p_dir + String("/") + fname; + if (!fname.begins_with("terrain3d") || !fname.ends_with(".res")) { + continue; + } + LOG(DEBUG, "Loading region from ", path); + Vector2i loc = Util::filename_to_location(fname); + if (loc.x == INT32_MAX) { + LOG(ERROR, "Cannot get region location from file name: ", fname); + continue; + } + Ref region = ResourceLoader::get_singleton()->load(path, "Terrain3DRegion", ResourceLoader::CACHE_MODE_IGNORE); + if (region.is_null()) { + LOG(ERROR, "Cannot load region at ", path); + continue; + } + region->take_over_path(path); + region->set_location(loc); + region->set_version(CURRENT_VERSION); // Sends upgrade warning if old version + add_region(region, false); + } + force_update_maps(); +} + /** * Returns the location of a terrain vertex at a certain LOD. If there is a hole at the position, it returns * NAN in the vector's Y coordinate. @@ -984,7 +885,6 @@ Ref Terrain3DStorage::layered_to_image(const MapType p_map_type) const { * p_global_position: X and Z coordinates of the vertex. Heights will be sampled around these coordinates. */ Vector3 Terrain3DStorage::get_mesh_vertex(const int32_t p_lod, const HeightFilter p_filter, const Vector3 &p_global_position) const { - IS_INIT_MESG("Storage not initialized", Vector3()); LOG(INFO, "Calculating vertex location"); int32_t step = 1 << CLAMP(p_lod, 0, 8); real_t height = 0.0f; @@ -1001,7 +901,7 @@ Vector3 Terrain3DStorage::get_mesh_vertex(const int32_t p_lod, const HeightFilte height = get_height(p_global_position); for (int32_t dx = -step / 2; dx < step / 2; dx += 1) { for (int32_t dz = -step / 2; dz < step / 2; dz += 1) { - Vector3 position = p_global_position + Vector3(dx, 0.f, dz) * _terrain->get_mesh_vertex_spacing(); + Vector3 position = p_global_position + Vector3(dx, 0.f, dz) * _mesh_vertex_spacing; if (is_hole(get_control(position))) { height = NAN; break; @@ -1018,22 +918,19 @@ Vector3 Terrain3DStorage::get_mesh_vertex(const int32_t p_lod, const HeightFilte } Vector3 Terrain3DStorage::get_normal(const Vector3 &p_global_position) const { - IS_INIT_MESG("Storage not initialized", Vector3()); - if (get_region_id(p_global_position) < 0 || is_hole(get_control(p_global_position))) { + if (get_region_idp(p_global_position) < 0 || is_hole(get_control(p_global_position))) { return Vector3(NAN, NAN, NAN); } - real_t vertex_spacing = _terrain->get_mesh_vertex_spacing(); real_t height = get_height(p_global_position); - real_t u = height - get_height(p_global_position + Vector3(vertex_spacing, 0.0f, 0.0f)); - real_t v = height - get_height(p_global_position + Vector3(0.f, 0.f, vertex_spacing)); - Vector3 normal = Vector3(u, vertex_spacing, v); + real_t u = height - get_height(p_global_position + Vector3(_mesh_vertex_spacing, 0.0f, 0.0f)); + real_t v = height - get_height(p_global_position + Vector3(0.f, 0.f, _mesh_vertex_spacing)); + Vector3 normal = Vector3(u, _mesh_vertex_spacing, v); normal.normalize(); return normal; } void Terrain3DStorage::print_audit_data() const { LOG(INFO, "Dumping storage data"); - LOG(INFO, "_modified: ", _modified); LOG(INFO, "Region_locations size: ", _region_locations.size(), " ", _region_locations); LOG(INFO, "Region map"); for (int i = 0; i < _region_map.size(); i++) { @@ -1055,59 +952,60 @@ void Terrain3DStorage::print_audit_data() const { /////////////////////////// void Terrain3DStorage::_bind_methods() { - BIND_ENUM_CONSTANT(TYPE_HEIGHT); - BIND_ENUM_CONSTANT(TYPE_CONTROL); - BIND_ENUM_CONSTANT(TYPE_COLOR); - BIND_ENUM_CONSTANT(TYPE_MAX); - - //BIND_ENUM_CONSTANT(SIZE_64); - //BIND_ENUM_CONSTANT(SIZE_128); - //BIND_ENUM_CONSTANT(SIZE_256); - //BIND_ENUM_CONSTANT(SIZE_512); - BIND_ENUM_CONSTANT(SIZE_1024); - //BIND_ENUM_CONSTANT(SIZE_2048); - BIND_ENUM_CONSTANT(HEIGHT_FILTER_NEAREST); BIND_ENUM_CONSTANT(HEIGHT_FILTER_MINIMUM); BIND_CONSTANT(REGION_MAP_SIZE); - ClassDB::bind_method(D_METHOD("set_version", "version"), &Terrain3DStorage::set_version); - ClassDB::bind_method(D_METHOD("get_version"), &Terrain3DStorage::get_version); - ClassDB::bind_method(D_METHOD("set_save_16_bit", "enabled"), &Terrain3DStorage::set_save_16_bit); - ClassDB::bind_method(D_METHOD("get_save_16_bit"), &Terrain3DStorage::get_save_16_bit); - - ClassDB::bind_method(D_METHOD("set_height_range", "range"), &Terrain3DStorage::set_height_range); - ClassDB::bind_method(D_METHOD("get_height_range"), &Terrain3DStorage::get_height_range); - ClassDB::bind_method(D_METHOD("update_height_range"), &Terrain3DStorage::update_height_range); - - ClassDB::bind_method(D_METHOD("set_region_size", "size"), &Terrain3DStorage::set_region_size); - ClassDB::bind_method(D_METHOD("get_region_size"), &Terrain3DStorage::get_region_size); + ClassDB::bind_method(D_METHOD("get_region_count"), &Terrain3DStorage::get_region_count); ClassDB::bind_method(D_METHOD("set_region_locations", "region_locations"), &Terrain3DStorage::set_region_locations); ClassDB::bind_method(D_METHOD("get_region_locations"), &Terrain3DStorage::get_region_locations); - ClassDB::bind_method(D_METHOD("get_region_count"), &Terrain3DStorage::get_region_count); + ClassDB::bind_method(D_METHOD("get_regions_active", "copy", "deep"), &Terrain3DStorage::get_regions_active, DEFVAL(false), DEFVAL(false)); + ClassDB::bind_method(D_METHOD("get_regions_all"), &Terrain3DStorage::get_regions_all); + ClassDB::bind_method(D_METHOD("get_region_map"), &Terrain3DStorage::get_region_map); + ClassDB::bind_static_method("Terrain3DStorage", D_METHOD("get_region_map_index"), &Terrain3DStorage::get_region_map_index); + + ClassDB::bind_method(D_METHOD("has_region", "region_location"), &Terrain3DStorage::has_region); + ClassDB::bind_method(D_METHOD("has_regionp", "global_position"), &Terrain3DStorage::has_regionp); + ClassDB::bind_method(D_METHOD("get_region", "region_location"), &Terrain3DStorage::get_region); + ClassDB::bind_method(D_METHOD("get_regionp", "global_position"), &Terrain3DStorage::get_regionp); + + ClassDB::bind_method(D_METHOD("set_region_modified", "region_location", "modified"), &Terrain3DStorage::set_region_modified); + ClassDB::bind_method(D_METHOD("is_region_modified", "region_location"), &Terrain3DStorage::is_region_modified); + ClassDB::bind_method(D_METHOD("set_region_deleted", "region_location", "deleted"), &Terrain3DStorage::set_region_deleted); + ClassDB::bind_method(D_METHOD("is_region_deleted", "region_location"), &Terrain3DStorage::is_region_deleted); + ClassDB::bind_method(D_METHOD("get_region_location", "global_position"), &Terrain3DStorage::get_region_location); - ClassDB::bind_method(D_METHOD("get_region_location_from_id", "region_id"), &Terrain3DStorage::get_region_location_from_id); - ClassDB::bind_method(D_METHOD("get_region_id", "global_position"), &Terrain3DStorage::get_region_id); - ClassDB::bind_method(D_METHOD("get_region_id_from_location", "region_location"), &Terrain3DStorage::get_region_id_from_location); - ClassDB::bind_method(D_METHOD("has_region", "global_position"), &Terrain3DStorage::has_region); - ClassDB::bind_method(D_METHOD("add_region", "global_position", "images", "update"), &Terrain3DStorage::add_region, DEFVAL(TypedArray()), DEFVAL(true)); - ClassDB::bind_method(D_METHOD("remove_region", "global_position", "update"), &Terrain3DStorage::remove_region, DEFVAL(true)); - - ClassDB::bind_method(D_METHOD("set_map_region", "map_type", "region_id", "image"), &Terrain3DStorage::set_map_region); - ClassDB::bind_method(D_METHOD("get_map_region", "map_type", "region_id"), &Terrain3DStorage::get_map_region); - ClassDB::bind_method(D_METHOD("set_maps", "map_type", "maps"), &Terrain3DStorage::set_maps); - ClassDB::bind_method(D_METHOD("get_maps", "map_type"), &Terrain3DStorage::get_maps); - ClassDB::bind_method(D_METHOD("get_maps_copy", "map_type"), &Terrain3DStorage::get_maps_copy); - ClassDB::bind_method(D_METHOD("set_height_maps", "maps"), &Terrain3DStorage::set_height_maps); + ClassDB::bind_method(D_METHOD("get_region_locationi", "region_id"), &Terrain3DStorage::get_region_locationi); + ClassDB::bind_method(D_METHOD("get_region_id", "region_location"), &Terrain3DStorage::get_region_id); + ClassDB::bind_method(D_METHOD("get_region_idp", "global_position"), &Terrain3DStorage::get_region_idp); + + ClassDB::bind_method(D_METHOD("add_region", "region", "update"), &Terrain3DStorage::add_region, DEFVAL(true)); + ClassDB::bind_method(D_METHOD("add_regionl", "region_location", "update"), &Terrain3DStorage::add_regionl, DEFVAL(true)); + ClassDB::bind_method(D_METHOD("add_regionp", "global_position", "update"), &Terrain3DStorage::add_regionp, DEFVAL(true)); + ClassDB::bind_method(D_METHOD("add_region_blank", "region_location", "update"), &Terrain3DStorage::add_region, DEFVAL(true)); + ClassDB::bind_method(D_METHOD("add_region_blankp", "global_position", "update"), &Terrain3DStorage::add_region, DEFVAL(true)); + + ClassDB::bind_method(D_METHOD("remove_region", "region", "update"), &Terrain3DStorage::remove_region, DEFVAL(true)); + ClassDB::bind_method(D_METHOD("remove_regionl", "region_location", "update"), &Terrain3DStorage::remove_regionl, DEFVAL(true)); + ClassDB::bind_method(D_METHOD("remove_regionp", "global_position", "update"), &Terrain3DStorage::remove_regionp, DEFVAL(true)); + + ClassDB::bind_method(D_METHOD("save_directory", "directory"), &Terrain3DStorage::save_directory); + ClassDB::bind_method(D_METHOD("load_directory", "directory"), &Terrain3DStorage::load_directory); + ClassDB::bind_method(D_METHOD("save_region", "directory", "region_location", "16_bit"), &Terrain3DStorage::save_region, DEFVAL(false)); + ClassDB::bind_method(D_METHOD("load_region", "directory", "region_location", "update"), &Terrain3DStorage::load_region, DEFVAL(true)); + ClassDB::bind_method(D_METHOD("get_height_maps"), &Terrain3DStorage::get_height_maps); - ClassDB::bind_method(D_METHOD("set_control_maps", "maps"), &Terrain3DStorage::set_control_maps); ClassDB::bind_method(D_METHOD("get_control_maps"), &Terrain3DStorage::get_control_maps); - ClassDB::bind_method(D_METHOD("set_color_maps", "maps"), &Terrain3DStorage::set_color_maps); ClassDB::bind_method(D_METHOD("get_color_maps"), &Terrain3DStorage::get_color_maps); ClassDB::bind_method(D_METHOD("get_height_maps_rid"), &Terrain3DStorage::get_height_maps_rid); ClassDB::bind_method(D_METHOD("get_control_maps_rid"), &Terrain3DStorage::get_control_maps_rid); ClassDB::bind_method(D_METHOD("get_color_maps_rid"), &Terrain3DStorage::get_color_maps_rid); + ClassDB::bind_method(D_METHOD("force_update_maps", "map_type", "generate_mipmaps"), &Terrain3DStorage::force_update_maps, DEFVAL(TYPE_MAX), DEFVAL(false)); + + ClassDB::bind_method(D_METHOD("get_maps", "map_type"), &Terrain3DStorage::get_maps); + ClassDB::bind_method(D_METHOD("get_maps_copy", "map_type"), &Terrain3DStorage::get_maps_copy); + ClassDB::bind_method(D_METHOD("set_pixel", "map_type", "global_position", "pixel"), &Terrain3DStorage::set_pixel); ClassDB::bind_method(D_METHOD("get_pixel", "map_type", "global_position"), &Terrain3DStorage::get_pixel); ClassDB::bind_method(D_METHOD("set_height", "global_position", "height"), &Terrain3DStorage::set_height); @@ -1118,40 +1016,24 @@ void Terrain3DStorage::_bind_methods() { ClassDB::bind_method(D_METHOD("get_control", "global_position"), &Terrain3DStorage::get_control); ClassDB::bind_method(D_METHOD("set_roughness", "global_position", "roughness"), &Terrain3DStorage::set_roughness); ClassDB::bind_method(D_METHOD("get_roughness", "global_position"), &Terrain3DStorage::get_roughness); - ClassDB::bind_method(D_METHOD("get_texture_id", "global_position"), &Terrain3DStorage::get_texture_id); ClassDB::bind_method(D_METHOD("get_angle", "global_position"), &Terrain3DStorage::get_angle); ClassDB::bind_method(D_METHOD("get_scale", "global_position"), &Terrain3DStorage::get_scale); - ClassDB::bind_method(D_METHOD("force_update_maps", "map_type"), &Terrain3DStorage::force_update_maps, DEFVAL(TYPE_MAX)); - ClassDB::bind_method(D_METHOD("set_multimeshes", "multimeshes"), &Terrain3DStorage::set_multimeshes); - ClassDB::bind_method(D_METHOD("get_multimeshes"), &Terrain3DStorage::get_multimeshes); + ClassDB::bind_method(D_METHOD("get_normal", "global_position"), &Terrain3DStorage::get_normal); + ClassDB::bind_method(D_METHOD("get_texture_id", "global_position"), &Terrain3DStorage::get_texture_id); + ClassDB::bind_method(D_METHOD("get_mesh_vertex", "lod", "filter", "global_position"), &Terrain3DStorage::get_mesh_vertex); + + ClassDB::bind_method(D_METHOD("get_height_range"), &Terrain3DStorage::get_height_range); + ClassDB::bind_method(D_METHOD("calc_height_range", "recursive"), &Terrain3DStorage::calc_height_range, DEFVAL(false)); - ClassDB::bind_method(D_METHOD("save"), &Terrain3DStorage::save); ClassDB::bind_method(D_METHOD("import_images", "images", "global_position", "offset", "scale"), &Terrain3DStorage::import_images, DEFVAL(Vector3(0, 0, 0)), DEFVAL(0.0), DEFVAL(1.0)); ClassDB::bind_method(D_METHOD("export_image", "file_name", "map_type"), &Terrain3DStorage::export_image); ClassDB::bind_method(D_METHOD("layered_to_image", "map_type"), &Terrain3DStorage::layered_to_image); - ClassDB::bind_method(D_METHOD("get_mesh_vertex", "lod", "filter", "global_position"), &Terrain3DStorage::get_mesh_vertex); - ClassDB::bind_method(D_METHOD("get_normal", "global_position"), &Terrain3DStorage::get_normal); - - int ro_flags = PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_READ_ONLY; - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "version", PROPERTY_HINT_NONE, "", ro_flags), "set_version", "get_version"); - //ADD_PROPERTY(PropertyInfo(Variant::INT, "region_size", PROPERTY_HINT_ENUM, "64:64, 128:128, 256:256, 512:512, 1024:1024, 2048:2048"), "set_region_size", "get_region_size"); - ADD_PROPERTY(PropertyInfo(Variant::INT, "region_size", PROPERTY_HINT_ENUM, "1024:1024"), "set_region_size", "get_region_size"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "save_16_bit", PROPERTY_HINT_NONE), "set_save_16_bit", "get_save_16_bit"); - ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "height_range", PROPERTY_HINT_NONE, "", ro_flags), "set_height_range", "get_height_range"); - ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "region_locations", PROPERTY_HINT_ARRAY_TYPE, vformat("%tex_size/%tex_size:%tex_size", Variant::VECTOR2, PROPERTY_HINT_NONE), ro_flags), "set_region_locations", "get_region_locations"); - ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "height_maps", PROPERTY_HINT_ARRAY_TYPE, vformat("%tex_size/%tex_size:%tex_size", Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE, "Image"), ro_flags), "set_height_maps", "get_height_maps"); - ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "control_maps", PROPERTY_HINT_ARRAY_TYPE, vformat("%tex_size/%tex_size:%tex_size", Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE, "Image"), ro_flags), "set_control_maps", "get_control_maps"); - ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "color_maps", PROPERTY_HINT_ARRAY_TYPE, vformat("%tex_size/%tex_size:%tex_size", Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE, "Image"), ro_flags), "set_color_maps", "get_color_maps"); - ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "multimeshes", PROPERTY_HINT_NONE, "", ro_flags), "set_multimeshes", "get_multimeshes"); - ADD_SIGNAL(MethodInfo("maps_changed")); ADD_SIGNAL(MethodInfo("region_map_changed")); ADD_SIGNAL(MethodInfo("height_maps_changed")); ADD_SIGNAL(MethodInfo("control_maps_changed")); ADD_SIGNAL(MethodInfo("color_maps_changed")); - ADD_SIGNAL(MethodInfo("region_size_changed")); ADD_SIGNAL(MethodInfo("maps_edited", PropertyInfo(Variant::AABB, "edited_area"))); - ADD_SIGNAL(MethodInfo("multimeshes_changed")); } diff --git a/src/terrain_3d_storage.h b/src/terrain_3d_storage.h index 6a59bd32..d675d8fd 100644 --- a/src/terrain_3d_storage.h +++ b/src/terrain_3d_storage.h @@ -3,63 +3,24 @@ #ifndef TERRAIN3D_STORAGE_CLASS_H #define TERRAIN3D_STORAGE_CLASS_H -#include -#include - #include "constants.h" #include "generated_texture.h" -#include "terrain_3d_util.h" +#include "terrain_3d_region.h" class Terrain3D; using namespace godot; -class Terrain3DStorage : public Resource { - GDCLASS(Terrain3DStorage, Resource); +class Terrain3DStorage : public Object { + GDCLASS(Terrain3DStorage, Object); CLASS_NAME(); + friend Terrain3D; public: // Constants - static inline const real_t CURRENT_VERSION = 0.92f; + static inline const real_t CURRENT_VERSION = 0.93f; static inline const int REGION_MAP_SIZE = 16; static inline const Vector2i REGION_MAP_VSIZE = Vector2i(REGION_MAP_SIZE, REGION_MAP_SIZE); - enum MapType { - TYPE_HEIGHT, - TYPE_CONTROL, - TYPE_COLOR, - TYPE_MAX, - }; - - static inline const Image::Format FORMAT[] = { - Image::FORMAT_RF, // TYPE_HEIGHT - Image::FORMAT_RF, // TYPE_CONTROL - Image::FORMAT_RGBA8, // TYPE_COLOR - Image::Format(TYPE_MAX), // Proper size of array instead of FORMAT_MAX - }; - - static inline const char *TYPESTR[] = { - "TYPE_HEIGHT", - "TYPE_CONTROL", - "TYPE_COLOR", - "TYPE_MAX", - }; - - static inline const Color COLOR[] = { - COLOR_BLACK, // TYPE_HEIGHT - COLOR_CONTROL, // TYPE_CONTROL - COLOR_ROUGHNESS, // TYPE_COLOR - COLOR_NAN, // TYPE_MAX, unused just in case someone indexes the array - }; - - enum RegionSize { - //SIZE_64 = 64, - //SIZE_128 = 128, - //SIZE_256 = 256, - //SIZE_512 = 512, - SIZE_1024 = 1024, - //SIZE_2048 = 2048, - }; - enum HeightFilter { HEIGHT_FILTER_NEAREST, HEIGHT_FILTER_MINIMUM @@ -68,97 +29,116 @@ class Terrain3DStorage : public Resource { private: Terrain3D *_terrain = nullptr; - // Work data - bool _modified = false; - bool _region_map_dirty = true; - PackedInt32Array _region_map; // 16x16 Region grid with index into region_locations (1 based array) - // Generated Texture RIDs - // These contain the TextureLayered RID from the RenderingServer, no Image - GeneratedTexture _generated_height_maps; - GeneratedTexture _generated_control_maps; - GeneratedTexture _generated_color_maps; + // Storage Settings & flags + int _region_size = 0; + Vector2i _region_sizev = Vector2i(_region_size, _region_size); + real_t _mesh_vertex_spacing = 1.f; // Set by Terrain3D::set_mesh_vertex_spacing AABB _edited_area; - uint64_t _last_region_bounds_error = 0; + Vector2 _master_height_range = V2_ZERO; + + ///////// + // Terrain3DRegions house the maps, instances, and other data for each region. + // Regions are dual indexed: + // 1) By `region_location:Vector2i` as the primary key. This is the only stable index + // so should be the main index for users. + // 2) By `region_id:int`. This index changes on every add/remove, depends on load order, + // and is not stable. It should not be relied on by users and is primarily for internal use. + + // `_regions` stores all loaded Terrain3DRegions, indexed by region_location. If marked for + // deletion they are removed from here upon saving, however they may stay in memory if tracked + // by the Undo system. + Dictionary _regions; // Dict[region_location:Vector2i] -> Terrain3DRegion + + // All _active_ region maps are maintained in these secondary indices. + // Regions are considered active if and only if they exist in `_region_locations`. The other + // arrays are built off of this index; its order defines region_id. + // The image arrays are converted to TextureArrays for the shader. - // Stored Data - real_t _version = 0.8f; // Set to ensure Godot always saves this - RegionSize _region_size = SIZE_1024; - Vector2i _region_sizev = Vector2i(_region_size, _region_size); - bool _save_16_bit = false; - Vector2 _height_range = Vector2(0.f, 0.f); - - /** - * These arrays house all of the map data. - * The Image arrays are region_sized slices of all heightmap data. Their world - * location is tracked by region_locations. The region data are combined into one large - * texture in generated_*_maps. - */ TypedArray _region_locations; TypedArray _height_maps; TypedArray _control_maps; TypedArray _color_maps; - // Foliage Instancer contains MultiMeshes saved to disk - // Dictionary[region_location:Vector2i] -> Dictionary[mesh_id:int] -> MultiMesh - Dictionary _multimeshes; + // Editing occurs on the Image arrays above, which are converted to Texture arrays + // below for the shader. + + // 16x16 grid with region_id:int at its location, no region = 0, region_ids >= 1 + PackedInt32Array _region_map; + bool _region_map_dirty = true; + + // These contain the TextureArray RIDs from the RenderingServer + GeneratedTexture _generated_height_maps; + GeneratedTexture _generated_control_maps; + GeneratedTexture _generated_color_maps; // Functions void _clear(); - int _get_region_map_index(const Vector2i &p_region_loc) const; public: Terrain3DStorage() {} void initialize(Terrain3D *p_terrain); - ~Terrain3DStorage(); - - void set_version(const real_t p_version); - real_t get_version() const { return _version; } - void set_save_16_bit(const bool p_enabled); - bool get_save_16_bit() const { return _save_16_bit; } + ~Terrain3DStorage() { _clear(); } - void set_height_range(const Vector2 &p_range); - Vector2 get_height_range() const { return _height_range; } - void update_heights(const real_t p_height); - void update_heights(const Vector2 &p_heights); - void update_height_range(); + /// Internal functions should be by region_id or region_location + /// External functions by region_location or global_position + /// look at godot's naming conventions + /// Conversion functions, many can probably be inline, static or in util - void clear_edited_area(); - void add_edited_area(const AABB &p_area); - AABB get_edited_area() const { return _edited_area; } + /// Regions - // Regions - void set_region_size(const RegionSize p_size); - RegionSize get_region_size() const { return _region_size; } - Vector2i get_region_sizev() const { return _region_sizev; } + int get_region_count() const { return _region_locations.size(); } void set_region_locations(const TypedArray &p_locations); TypedArray get_region_locations() const { return _region_locations; } + TypedArray get_regions_active(const bool p_copy = false, const bool p_deep = false) const; + Dictionary get_regions_all() const { return _regions; } PackedInt32Array get_region_map() const { return _region_map; } - int get_region_count() const { return _region_locations.size(); } + static int get_region_map_index(const Vector2i &p_region_loc); + + bool has_region(const Vector2i &p_region_loc) const { return get_region_id(p_region_loc) != -1; } + bool has_regionp(const Vector3 &p_global_position) const { return get_region_idp(p_global_position) != -1; } + Ref get_region(const Vector2i &p_region_loc) const { return _regions[p_region_loc]; } + Ref get_regionp(const Vector3 &p_global_position) const { return _regions[get_region_location(p_global_position)]; } + + void set_region_modified(const Vector2i &p_region_loc, const bool p_modified = true); + bool is_region_modified(const Vector2i &p_region_loc) const; + void set_region_deleted(const Vector2i &p_region_loc, const bool p_deleted = true); + bool is_region_deleted(const Vector2i &p_region_loc) const; + Vector2i get_region_location(const Vector3 &p_global_position) const; - Vector2i get_region_location_from_id(const int p_region_id) const; - int get_region_id(const Vector3 &p_global_position) const; - int get_region_id_from_location(const Vector2i &p_region_loc) const; - bool has_region(const Vector3 &p_global_position) const { return get_region_id(p_global_position) != -1; } - Error add_region(const Vector3 &p_global_position, const TypedArray &p_images = TypedArray(), const bool p_update = true); - void remove_region(const Vector3 &p_global_position, const bool p_update = true); - void update_maps(); + Vector2i get_region_locationi(const int p_region_id) const; + int get_region_id(const Vector2i &p_region_loc) const; + int get_region_idp(const Vector3 &p_global_position) const; + + Error add_region(const Ref &p_region, const bool p_update = true); + Error add_regionl(const Vector2i &p_region_loc, const Ref &p_region, const bool p_update = true); + Error add_regionp(const Vector3 &p_global_position, const Ref &p_region, const bool p_update = true); + Ref add_region_blank(const Vector2i &p_region_loc, const bool p_update = true); + Ref add_region_blankp(const Vector3 &p_global_position, const bool p_update = true); + void remove_region(const Ref &p_region, const bool p_update = true); + void remove_regionl(const Vector2i &p_region_loc, const bool p_update = true); + void remove_regionp(const Vector3 &p_global_position, const bool p_update = true); + + // File I/O + void save_directory(const String &p_dir); + void load_directory(const String &p_dir); + void save_region(const Vector2i &p_region_loc, const String &p_dir, const bool p_16_bit = false); + void load_region(const Vector2i &p_region_loc, const String &p_dir, const bool p_update = true); // Maps - void set_map_region(const MapType p_map_type, const int p_region_id, const Ref &p_image); - Ref get_map_region(const MapType p_map_type, const int p_region_id) const; - void set_maps(const MapType p_map_type, const TypedArray &p_maps); - TypedArray get_maps(const MapType p_map_type) const; - TypedArray get_maps_copy(const MapType p_map_type) const; - void set_height_maps(const TypedArray &p_maps) { set_maps(TYPE_HEIGHT, p_maps); } TypedArray get_height_maps() const { return _height_maps; } - RID get_height_maps_rid() const { return _generated_height_maps.get_rid(); } - void set_control_maps(const TypedArray &p_maps) { set_maps(TYPE_CONTROL, p_maps); } TypedArray get_control_maps() const { return _control_maps; } - RID get_control_maps_rid() const { return _generated_control_maps.get_rid(); } - void set_color_maps(const TypedArray &p_maps) { set_maps(TYPE_COLOR, p_maps); } TypedArray get_color_maps() const { return _color_maps; } + RID get_height_maps_rid() const { return _generated_height_maps.get_rid(); } + RID get_control_maps_rid() const { return _generated_control_maps.get_rid(); } RID get_color_maps_rid() const { return _generated_color_maps.get_rid(); } + + void update_maps(); + void force_update_maps(const MapType p_map = TYPE_MAX, const bool p_generate_mipmaps = false); + + TypedArray get_maps(const MapType p_map_type) const; + TypedArray get_maps_copy(const MapType p_map_type, const TypedArray &p_region_ids = TypedArray()) const; + void set_pixel(const MapType p_map_type, const Vector3 &p_global_position, const Color &p_pixel); Color get_pixel(const MapType p_map_type, const Vector3 &p_global_position) const; void set_height(const Vector3 &p_global_position, const real_t p_height); @@ -169,57 +149,84 @@ class Terrain3DStorage : public Resource { uint32_t get_control(const Vector3 &p_global_position) const; void set_roughness(const Vector3 &p_global_position, const real_t p_roughness); real_t get_roughness(const Vector3 &p_global_position) const; - Vector3 get_texture_id(const Vector3 &p_global_position) const; real_t get_angle(const Vector3 &p_global_position) const; real_t get_scale(const Vector3 &p_global_position) const; - TypedArray sanitize_maps(const MapType p_map_type, const TypedArray &p_maps) const; - void force_update_maps(const MapType p_map = TYPE_MAX); - // Instancer - void set_multimeshes(const Dictionary &p_multimeshes); - Dictionary get_multimeshes() const { return _multimeshes; } + Vector3 get_normal(const Vector3 &global_position) const; + Vector3 get_texture_id(const Vector3 &p_global_position) const; + Vector3 get_mesh_vertex(const int32_t p_lod, const HeightFilter p_filter, const Vector3 &p_global_position) const; - // File I/O - void save(); - void clear_modified() { _modified = false; } - void set_modified() { _modified = true; } - void import_images(const TypedArray &p_images, const Vector3 &p_global_position = Vector3(0.f, 0.f, 0.f), + void clear_edited_area(); + void add_edited_area(const AABB &p_area); + AABB get_edited_area() const { return _edited_area; } + + Vector2 get_height_range() const { return _master_height_range; } + void update_master_height(const real_t p_height); + void update_master_heights(const Vector2 &p_low_high); + void calc_height_range(const bool p_recursive = false); + + void import_images(const TypedArray &p_images, const Vector3 &p_global_position = V3_ZERO, const real_t p_offset = 0.f, const real_t p_scale = 1.f); Error export_image(const String &p_file_name, const MapType p_map_type = TYPE_HEIGHT) const; Ref layered_to_image(const MapType p_map_type) const; // Utility - Vector3 get_mesh_vertex(const int32_t p_lod, const HeightFilter p_filter, const Vector3 &p_global_position) const; - Vector3 get_normal(const Vector3 &global_position) const; void print_audit_data() const; protected: static void _bind_methods(); }; -typedef Terrain3DStorage::MapType MapType; -VARIANT_ENUM_CAST(Terrain3DStorage::MapType); -constexpr Terrain3DStorage::MapType TYPE_HEIGHT = Terrain3DStorage::MapType::TYPE_HEIGHT; -constexpr Terrain3DStorage::MapType TYPE_CONTROL = Terrain3DStorage::MapType::TYPE_CONTROL; -constexpr Terrain3DStorage::MapType TYPE_COLOR = Terrain3DStorage::MapType::TYPE_COLOR; -constexpr Terrain3DStorage::MapType TYPE_MAX = Terrain3DStorage::MapType::TYPE_MAX; -VARIANT_ENUM_CAST(Terrain3DStorage::RegionSize); VARIANT_ENUM_CAST(Terrain3DStorage::HeightFilter); -/// Inline Functions - -// This function verifies the location is within the bounds of the -// _region_map array and returns the map index if valid, -1 if not -inline int Terrain3DStorage::_get_region_map_index(const Vector2i &p_region_loc) const { - // Offset locations centered on (0,0) to positive only - Vector2i loc = Vector2i(p_region_loc + (REGION_MAP_VSIZE / 2)); - int map_index = loc.y * REGION_MAP_SIZE + loc.x; - if (map_index < 0 || map_index >= REGION_MAP_SIZE * REGION_MAP_SIZE) { +/// Inline Region Functions + +// Verifies the location is within the bounds of the _region_map array and +// the world, returning the _region_map index, which contains the region_id. +// Valid region locations are -8, -8 to 7, 7, or when offset: 0, 0 to 15, 15 +// If any bits other than 0xF are set, it's out of bounds and returns -1 +inline int Terrain3DStorage::get_region_map_index(const Vector2i &p_region_loc) { + // Offset world to positive values only + Vector2i loc = p_region_loc + (REGION_MAP_VSIZE / 2); + // Catch values > 15 + if ((uint32_t(loc.x | loc.y) & uint32_t(~0xF)) > 0) { return -1; } - return map_index; + return loc.y * REGION_MAP_SIZE + loc.x; +} + +// Returns a region location given a global position. No bounds checking nor data access. +inline Vector2i Terrain3DStorage::get_region_location(const Vector3 &p_global_position) const { + Vector2 descaled_position = Vector2(p_global_position.x, p_global_position.z); + return Vector2i((descaled_position / (_mesh_vertex_spacing * real_t(_region_size))).floor()); +} + +// Returns Vector2i(2147483647) if out of range +inline Vector2i Terrain3DStorage::get_region_locationi(const int p_region_id) const { + if (p_region_id < 0 || p_region_id >= _region_locations.size()) { + return V2I_MAX; + } + return _region_locations[p_region_id]; } +// Returns id of any active region. -1 if out of bounds, 0 if no region, or region id +inline int Terrain3DStorage::get_region_id(const Vector2i &p_region_loc) const { + int map_index = get_region_map_index(p_region_loc); + if (map_index >= 0) { + int region_id = _region_map[map_index] - 1; // 0 = no region + if (region_id >= 0 && region_id < _region_locations.size()) { + return region_id; + } + } + return -1; +} + +inline int Terrain3DStorage::get_region_idp(const Vector3 &p_global_position) const { + return get_region_id(get_region_location(p_global_position)); +} + +/// Inline Map Functions + inline void Terrain3DStorage::set_height(const Vector3 &p_global_position, const real_t p_height) { set_pixel(TYPE_HEIGHT, p_global_position, Color(p_height, 0.f, 0.f, 1.f)); } @@ -255,4 +262,21 @@ inline real_t Terrain3DStorage::get_roughness(const Vector3 &p_global_position) return get_pixel(TYPE_COLOR, p_global_position).a; } +inline void Terrain3DStorage::update_master_height(const real_t p_height) { + if (p_height < _master_height_range.x) { + _master_height_range.x = p_height; + } else if (p_height > _master_height_range.y) { + _master_height_range.y = p_height; + } +} + +inline void Terrain3DStorage::update_master_heights(const Vector2 &p_low_high) { + if (p_low_high.x < _master_height_range.x) { + _master_height_range.x = p_low_high.x; + } + if (p_low_high.y > _master_height_range.y) { + _master_height_range.y = p_low_high.y; + } +} + #endif // TERRAIN3D_STORAGE_CLASS_H diff --git a/src/terrain_3d_texture_asset.cpp b/src/terrain_3d_texture_asset.cpp index 53ad2111..cb2acb58 100644 --- a/src/terrain_3d_texture_asset.cpp +++ b/src/terrain_3d_texture_asset.cpp @@ -37,9 +37,6 @@ Terrain3DTextureAsset::Terrain3DTextureAsset() { clear(); } -Terrain3DTextureAsset::~Terrain3DTextureAsset() { -} - void Terrain3DTextureAsset::clear() { _name = "New Texture"; _id = 0; diff --git a/src/terrain_3d_texture_asset.h b/src/terrain_3d_texture_asset.h index 9573978d..1f68b6cb 100644 --- a/src/terrain_3d_texture_asset.h +++ b/src/terrain_3d_texture_asset.h @@ -25,7 +25,7 @@ class Terrain3DTextureAsset : public Terrain3DAssetResource { public: Terrain3DTextureAsset(); - ~Terrain3DTextureAsset(); + ~Terrain3DTextureAsset() {} void clear() override; diff --git a/src/terrain_3d_util.cpp b/src/terrain_3d_util.cpp index 85d5599a..3122c6a4 100644 --- a/src/terrain_3d_util.cpp +++ b/src/terrain_3d_util.cpp @@ -10,11 +10,56 @@ // Public Functions /////////////////////////// +void Terrain3DUtil::print_arr(const String &p_name, const Array &p_arr, const int p_level) { + LOG(p_level, "Array[", p_arr.size(), "]: ", p_name); + for (int i = 0; i < p_arr.size(); i++) { + Variant var = p_arr[i]; + switch (var.get_type()) { + case Variant::ARRAY: { + print_arr(p_name + String::num_int64(i), var, p_level); + break; + } + case Variant::DICTIONARY: { + print_dict(p_name + String::num_int64(i), var, p_level); + break; + } + case Variant::OBJECT: { + String inst = "Object#" + String::num_uint64(cast_to(var)->get_instance_id()); + LOG(p_level, i, ": ", inst); + break; + } + default: { + LOG(p_level, i, ": ", p_arr[i]); + break; + } + } + } +} + void Terrain3DUtil::print_dict(const String &p_name, const Dictionary &p_dict, const int p_level) { - LOG(p_level, "Printing Dictionary: ", p_name); + LOG(p_level, "Dictionary: ", p_name); Array keys = p_dict.keys(); for (int i = 0; i < keys.size(); i++) { - LOG(p_level, "Key: ", keys[i], ", Value: ", p_dict[keys[i]]); + Variant var = p_dict[keys[i]]; + switch (var.get_type()) { + case Variant::ARRAY: { + print_arr(p_name + String::num_int64(i), var, p_level); + break; + } + case Variant::DICTIONARY: { + print_dict(p_name + String::num_int64(i), var, p_level); + break; + } + case Variant::OBJECT: { + String inst = "Object#" + String::num_uint64(cast_to(var)->get_instance_id()); + LOG(p_level, "\"", keys[i], "\": ", inst); + break; + } + default: { + LOG(p_level, "\"", keys[i], "\": Value: ", var); + break; + } + } } } @@ -30,6 +75,28 @@ void Terrain3DUtil::dump_maps(const TypedArray &p_maps, const String &p_n } } +// Expects a filename in a String like: "terrain3d-01_02.res" which returns (-1, 2) +Vector2i Terrain3DUtil::filename_to_location(const String &p_filename) { + String working_string = p_filename.trim_suffix(".res"); + String y_str = working_string.right(3).replace("_", ""); + working_string = working_string.erase(working_string.length() - 3, 3); + String x_str = working_string.right(3).replace("_", ""); + if (!x_str.is_valid_int() || !y_str.is_valid_int()) { + LOG(ERROR, "Malformed filename at ", p_filename, ": got x ", x_str, " y ", y_str); + return V2I_MAX; + } + return Vector2i(x_str.to_int(), y_str.to_int()); +} + +String Terrain3DUtil::location_to_filename(const Vector2i &p_region_loc) { + const String POS_REGION_FORMAT = "_%02d"; + const String NEG_REGION_FORMAT = "%03d"; + String x_str, y_str; + x_str = vformat((p_region_loc.x >= 0) ? POS_REGION_FORMAT : NEG_REGION_FORMAT, p_region_loc.x); + y_str = vformat((p_region_loc.y >= 0) ? POS_REGION_FORMAT : NEG_REGION_FORMAT, p_region_loc.y); + return "terrain3d" + x_str + y_str + ".res"; +} + Ref Terrain3DUtil::black_to_alpha(const Ref &p_image) { if (p_image.is_null()) { return Ref(); @@ -57,7 +124,7 @@ Vector2 Terrain3DUtil::get_min_max(const Ref &p_image) { return Vector2(INFINITY, INFINITY); } - Vector2 min_max = Vector2(0.f, 0.f); + Vector2 min_max = V2_ZERO; for (int y = 0; y < p_image->get_height(); y++) { for (int x = 0; x < p_image->get_width(); x++) { @@ -178,7 +245,7 @@ Ref Terrain3DUtil::get_filled_image(const Vector2i &p_size, const Color & color.a = 1.0f; Color col_a = Color(0.8f, 0.8f, 0.8f, 1.0) * color; Color col_b = Color(0.5f, 0.5f, 0.5f, 1.0) * color; - img->fill_rect(Rect2i(Vector2i(0, 0), p_size / 2), col_a); + img->fill_rect(Rect2i(V2I_ZERO, p_size / 2), col_a); img->fill_rect(Rect2i(p_size / 2, p_size / 2), col_a); img->fill_rect(Rect2i(Vector2(p_size.x, 0) / 2, p_size / 2), col_b); img->fill_rect(Rect2i(Vector2(0, p_size.y) / 2, p_size / 2), col_b); @@ -225,7 +292,7 @@ Ref Terrain3DUtil::load_image(const String &p_file_name, const int p_cach Ref file = FileAccess::open(p_file_name, FileAccess::READ); // If p_size is zero, assume square and try to auto detect size Vector2i r16_size = p_r16_size; - if (r16_size <= Vector2i(0, 0)) { + if (r16_size <= V2I_ZERO) { file->seek_end(); int fsize = file->get_position(); int fwidth = sqrt(fsize / 2); @@ -233,7 +300,7 @@ Ref Terrain3DUtil::load_image(const String &p_file_name, const int p_cach LOG(DEBUG, "Total file size is: ", fsize, " calculated width: ", fwidth, " dimensions: ", r16_size); file->seek(0); } - img = Image::create(r16_size.x, r16_size.y, false, Terrain3DStorage::FORMAT[TYPE_HEIGHT]); + img = Image::create(r16_size.x, r16_size.y, false, FORMAT[TYPE_HEIGHT]); for (int y = 0; y < r16_size.y; y++) { for (int x = 0; x < r16_size.x; x++) { real_t h = real_t(file->get_16()) / 65535.0f; @@ -254,7 +321,7 @@ Ref Terrain3DUtil::load_image(const String &p_file_name, const int p_cach } if (!img.is_valid()) { - LOG(ERROR, "File", p_file_name, " could not be loaded."); + LOG(ERROR, "File", p_file_name, " cannot be loaded."); return Ref(); } if (img->is_empty()) { @@ -321,11 +388,15 @@ void Terrain3DUtil::_bind_methods() { ClassDB::bind_static_method("Terrain3DUtil", D_METHOD("get_uv_scale", "pixel"), &gd_get_uv_scale); ClassDB::bind_static_method("Terrain3DUtil", D_METHOD("enc_uv_scale", "scale"), &gd_enc_uv_scale); + // String functions + ClassDB::bind_static_method("Terrain3DUtil", D_METHOD("filename_to_location", "filename"), &Terrain3DUtil::filename_to_location); + ClassDB::bind_static_method("Terrain3DUtil", D_METHOD("location_to_filename", "region_location"), &Terrain3DUtil::location_to_filename); + // Image handling ClassDB::bind_static_method("Terrain3DUtil", D_METHOD("black_to_alpha", "image"), &Terrain3DUtil::black_to_alpha); ClassDB::bind_static_method("Terrain3DUtil", D_METHOD("get_min_max", "image"), &Terrain3DUtil::get_min_max); ClassDB::bind_static_method("Terrain3DUtil", D_METHOD("get_thumbnail", "image", "size"), &Terrain3DUtil::get_thumbnail, DEFVAL(Vector2i(256, 256))); ClassDB::bind_static_method("Terrain3DUtil", D_METHOD("get_filled_image", "size", "color", "create_mipmaps", "format"), &Terrain3DUtil::get_filled_image); - ClassDB::bind_static_method("Terrain3DUtil", D_METHOD("load_image", "file_name", "cache_mode", "r16_height_range", "r16_size"), &Terrain3DUtil::load_image, DEFVAL(ResourceLoader::CACHE_MODE_IGNORE), DEFVAL(Vector2(0, 255)), DEFVAL(Vector2i(0, 0))); + ClassDB::bind_static_method("Terrain3DUtil", D_METHOD("load_image", "file_name", "cache_mode", "r16_height_range", "r16_size"), &Terrain3DUtil::load_image, DEFVAL(ResourceLoader::CACHE_MODE_IGNORE), DEFVAL(Vector2(0, 255)), DEFVAL(V2I_ZERO)); ClassDB::bind_static_method("Terrain3DUtil", D_METHOD("pack_image", "src_rgb", "src_r", "invert_green_channel"), &Terrain3DUtil::pack_image, DEFVAL(false)); } diff --git a/src/terrain_3d_util.h b/src/terrain_3d_util.h index 9e32f65b..e7a43b5c 100644 --- a/src/terrain_3d_util.h +++ b/src/terrain_3d_util.h @@ -4,22 +4,33 @@ #define TERRAIN3D_UTIL_CLASS_H #include +#include #include "constants.h" #include "generated_texture.h" using namespace godot; +// This file holds stateless utility functions for both C++ and GDScript +// The class exposes static member and inline functions to GDscript +// The inline functions below are not part of the class eg bilerp +// However some of these inline functions are also exposed to GDScript + class Terrain3DUtil : public Object { GDCLASS(Terrain3DUtil, Object); CLASS_NAME_STATIC("Terrain3DUtil"); public: // Print info to the console - static void print_dict(const String &name, const Dictionary &p_dict, const int p_level = 2); // Level 2: DEBUG + static void print_arr(const String &p_name, const Array &p_arr, const int p_level = 2); // Level 2: DEBUG + static void print_dict(const String &p_name, const Dictionary &p_dict, const int p_level = 2); // Level 2: DEBUG static void dump_gentex(const GeneratedTexture p_gen, const String &name = "", const int p_level = 2); static void dump_maps(const TypedArray &p_maps, const String &p_name = ""); + // String functions + static Vector2i filename_to_location(const String &p_filename); + static String location_to_filename(const Vector2i &p_region_loc); + // Image operations static Ref black_to_alpha(const Ref &p_image); static Vector2 get_min_max(const Ref &p_image); @@ -29,7 +40,7 @@ class Terrain3DUtil : public Object { const bool p_create_mipmaps = true, const Image::Format p_format = Image::FORMAT_MAX); static Ref load_image(const String &p_file_name, const int p_cache_mode = ResourceLoader::CACHE_MODE_IGNORE, - const Vector2 &p_r16_height_range = Vector2(0.f, 255.f), const Vector2i &p_r16_size = Vector2i(0, 0)); + const Vector2 &p_r16_height_range = Vector2(0.f, 255.f), const Vector2i &p_r16_size = V2I_ZERO); static Ref pack_image(const Ref &p_src_rgb, const Ref &p_src_r, const bool p_invert_green_channel = false); protected: @@ -44,6 +55,7 @@ typedef Terrain3DUtil Util; // Math /////////////////////////// +// Rounds a decimal to the nearest multiple eg round_multiple(2.7, 4) -> 4 template T round_multiple(const T p_value, const T p_multiple) { if (p_multiple == 0) { @@ -52,6 +64,10 @@ T round_multiple(const T p_value, const T p_multiple) { return static_cast(std::round(static_cast(p_value) / static_cast(p_multiple)) * static_cast(p_multiple)); } +inline bool is_power_of_2(const int p_n) { + return p_n && !(p_n & (p_n - 1)); +} + // Returns the bilinearly interpolated value derived from parameters: // * 4 values to be interpolated // * Positioned at the 4 corners of the p_pos00 - p_pos11 rectangle