diff --git a/project/addons/terrain_3d/src/asset_dock.gd b/project/addons/terrain_3d/src/asset_dock.gd index d9bc1f683..6e07b096b 100644 --- a/project/addons/terrain_3d/src/asset_dock.gd +++ b/project/addons/terrain_3d/src/asset_dock.gd @@ -204,7 +204,8 @@ func _on_godot_window_entered() -> void: func set_selected_by_asset_id(p_id: int) -> void: search_box.text = "" _on_search_text_changed() - current_list.set_selected_id(p_id) + current_list.selected_asset_ids = [p_id] + current_list.update_selection() func _on_search_text_changed() -> void: @@ -221,7 +222,7 @@ func _on_search_text_changed() -> void: mesh_list.search_text = search_box.text texture_list.search_text = search_box.text current_list.update_asset_list() - current_list.set_selected_id(0) + current_list.update_selection() func _on_search_button_pressed() -> void: @@ -369,7 +370,7 @@ class ListContainer extends Container: var plugin: EditorPlugin var type := Terrain3DAssets.TYPE_TEXTURE var entries: Array[ListEntry] - var selected_id: int = 0 + var selected_asset_ids: Array[int] = [0] var height: float = 0. var width: float = 90. var focus_style: StyleBox @@ -416,10 +417,11 @@ class ListContainer extends Container: var mesh_count: int = t.assets.get_mesh_count() for i in mesh_count: var mesh: Terrain3DMeshAsset = t.assets.get_mesh_asset(i) - add_item(mesh) + if mesh: + add_item(mesh) if mesh_count < Terrain3DAssets.MAX_MESHES: add_item() - set_selected_id(selected_id) + update_selection() func add_item(p_resource: Resource = null) -> void: @@ -451,49 +453,44 @@ class ListContainer extends Container: func set_selected_after_swap(p_type: Terrain3DAssets.AssetType, p_old_id: int, p_new_id: int) -> void: EditorInterface.mark_scene_as_unsaved() - set_selected_id(clamp(p_new_id, 0, entries.size() - 2)) + # Select the new id + clicked_id(MOUSE_BUTTON_LEFT, p_new_id) + # deselect the old id + clicked_id(MOUSE_BUTTON_LEFT, p_old_id) + update_selection() - func clicked_id(p_id: int) -> void: + func clicked_id(p_mouse_button: MouseButton, p_id: int) -> void: # Select Tool if clicking an asset plugin.select_terrain() - if type == Terrain3DAssets.TYPE_TEXTURE and \ - not plugin.editor.get_tool() in [ Terrain3DEditor.TEXTURE, Terrain3DEditor.COLOR, Terrain3DEditor.ROUGHNESS ]: - plugin.ui.toolbar.change_tool("PaintTexture") - elif type == Terrain3DAssets.TYPE_MESH and plugin.editor.get_tool() != Terrain3DEditor.INSTANCER: - plugin.ui.toolbar.change_tool("InstanceMeshes") - set_selected_id(p_id) - + if type == Terrain3DAssets.TYPE_TEXTURE: + if not plugin.editor.get_tool() in [ Terrain3DEditor.TEXTURE, Terrain3DEditor.COLOR, Terrain3DEditor.ROUGHNESS ]: + plugin.ui.toolbar.change_tool("PaintTexture") + var res: Terrain3DTextureAsset = entries[p_id].resource + selected_asset_ids = [ res.get_id() ] + elif type == Terrain3DAssets.TYPE_MESH: + if plugin.editor.get_tool() != Terrain3DEditor.INSTANCER: + plugin.ui.toolbar.change_tool("InstanceMeshes") + var res: Terrain3DMeshAsset = entries[p_id].resource + + if selected_asset_ids.has(res.get_id()): + if not p_mouse_button == MOUSE_BUTTON_RIGHT: + selected_asset_ids.erase(res.get_id()) + else: + selected_asset_ids.push_back(res.get_id()) + if selected_asset_ids.is_empty(): + selected_asset_ids = [0] + update_selection() - func set_selected_id(p_id: int) -> void: - # "Add new" is the final entry only when search box is blank - var max_id: int = max(0, entries.size() - (1 if search_text else 2)) - if plugin.debug: - print("Terrain3DListContainer ", name, ": set_selected_id: ", selected_id, " to ", clamp(p_id, 0, max_id)) - selected_id = clamp(p_id, 0, max_id) + + func update_selection() -> void: for i in entries.size(): var entry: ListEntry = entries[i] - entry.set_selected(i == selected_id) + if entry.resource: + entry.set_selected(entry.resource.get_id() in selected_asset_ids) plugin.ui._on_setting_changed() - func get_selected_asset_id() -> int: - # "Add new" is the final entry only when search box is blank - var max_id: int = max(0, entries.size() - (1 if search_text else 2)) - var id: int = clamp(selected_id, 0, max_id) - if plugin.debug: - print("Terrain3DListContainer ", name, ": get_selected_asset_id: selected_id: ", selected_id, ", clamped: ", id, ", entries: ", entries.size()) - if id >= entries.size(): - return 0 - var res: Resource = entries[id].resource - if not res: - return 0 - if type == Terrain3DAssets.AssetType.TYPE_MESH: - return (res as Terrain3DMeshAsset).id - else: - return (res as Terrain3DTextureAsset).id - - func _on_resource_inspected(p_resource: Resource) -> void: await get_tree().process_frame EditorInterface.edit_resource(p_resource) @@ -582,7 +579,7 @@ class ListContainer extends Container: class ListEntry extends MarginContainer: signal hovered() - signal clicked() + signal clicked(mouse_button: MouseButton) signal changed(resource: Resource) signal inspected(resource: Resource) @@ -839,7 +836,7 @@ class ListEntry extends MarginContainer: set_edited_resource(Terrain3DMeshAsset.new(), false) _on_edit() else: - emit_signal("clicked") + clicked.emit(MOUSE_BUTTON_LEFT) MOUSE_BUTTON_RIGHT: if resource: _on_edit() @@ -900,7 +897,7 @@ class ListEntry extends MarginContainer: button_clear.set_visible(resource != null) queue_redraw() - if not p_no_signal: + if resource and not p_no_signal: emit_signal("changed", resource) @@ -930,7 +927,7 @@ class ListEntry extends MarginContainer: func _on_edit() -> void: - emit_signal("clicked") + clicked.emit(MOUSE_BUTTON_RIGHT) emit_signal("inspected", resource) diff --git a/project/addons/terrain_3d/src/asset_dock_45.gd b/project/addons/terrain_3d/src/asset_dock_45.gd index 990f914a1..eff295412 100644 --- a/project/addons/terrain_3d/src/asset_dock_45.gd +++ b/project/addons/terrain_3d/src/asset_dock_45.gd @@ -255,7 +255,8 @@ func update_layout() -> void: func set_selected_by_asset_id(p_id: int) -> void: search_box.text = "" _on_search_text_changed() - current_list.set_selected_id(p_id) + current_list.selected_asset_ids = [p_id] + current_list.update_selection() func _on_search_text_changed() -> void: @@ -272,7 +273,8 @@ func _on_search_text_changed() -> void: mesh_list.search_text = search_box.text texture_list.search_text = search_box.text current_list.update_asset_list() - current_list.set_selected_id(0) + current_list.update_selection() + func _on_search_button_pressed() -> void: @@ -515,7 +517,7 @@ class ListContainer extends Container: var plugin: EditorPlugin var type := Terrain3DAssets.TYPE_TEXTURE var entries: Array[ListEntry] - var selected_id: int = 0 + var selected_asset_ids: Array[int] = [0] var height: float = 0. var width: float = 90. var focus_style: StyleBox @@ -565,7 +567,7 @@ class ListContainer extends Container: add_item(mesh) if mesh_count < Terrain3DAssets.MAX_MESHES: add_item() - set_selected_id(selected_id) + update_selection() func add_item(p_resource: Resource = null) -> void: @@ -597,49 +599,44 @@ class ListContainer extends Container: func set_selected_after_swap(p_type: Terrain3DAssets.AssetType, p_old_id: int, p_new_id: int) -> void: EditorInterface.mark_scene_as_unsaved() - set_selected_id(clamp(p_new_id, 0, entries.size() - 2)) + # Select the new id + clicked_id(MOUSE_BUTTON_LEFT, p_new_id) + # deselect the old id + clicked_id(MOUSE_BUTTON_LEFT, p_old_id) + update_selection() - func clicked_id(p_id: int) -> void: + func clicked_id(p_mouse_button: MouseButton, p_id: int) -> void: # Select Tool if clicking an asset plugin.select_terrain() - if type == Terrain3DAssets.TYPE_TEXTURE and \ - not plugin.editor.get_tool() in [ Terrain3DEditor.TEXTURE, Terrain3DEditor.COLOR, Terrain3DEditor.ROUGHNESS ]: - plugin.ui.toolbar.change_tool("PaintTexture") - elif type == Terrain3DAssets.TYPE_MESH and plugin.editor.get_tool() != Terrain3DEditor.INSTANCER: - plugin.ui.toolbar.change_tool("InstanceMeshes") - set_selected_id(p_id) + if type == Terrain3DAssets.TYPE_TEXTURE: + if not plugin.editor.get_tool() in [ Terrain3DEditor.TEXTURE, Terrain3DEditor.COLOR, Terrain3DEditor.ROUGHNESS ]: + plugin.ui.toolbar.change_tool("PaintTexture") + var res: Terrain3DTextureAsset = entries[p_id].resource + selected_asset_ids = [ res.get_id() ] + elif type == Terrain3DAssets.TYPE_MESH: + if plugin.editor.get_tool() != Terrain3DEditor.INSTANCER: + plugin.ui.toolbar.change_tool("InstanceMeshes") + var res: Terrain3DMeshAsset = entries[p_id].resource + + if selected_asset_ids.has(res.get_id()): + if not p_mouse_button == MOUSE_BUTTON_RIGHT: + selected_asset_ids.erase(res.get_id()) + else: + selected_asset_ids.push_back(res.get_id()) + if selected_asset_ids.is_empty(): + selected_asset_ids = [0] + update_selection() - func set_selected_id(p_id: int) -> void: - # "Add new" is the final entry only when search box is blank - var max_id: int = max(0, entries.size() - (1 if search_text else 2)) - if plugin.debug: - print("Terrain3DListContainer ", name, ": set_selected_id: ", selected_id, " to ", clamp(p_id, 0, max_id)) - selected_id = clamp(p_id, 0, max_id) + func update_selection() -> void: for i in entries.size(): var entry: ListEntry = entries[i] - entry.set_selected(i == selected_id) + if entry.resource: + entry.set_selected(entry.resource.get_id() in selected_asset_ids) plugin.ui._on_setting_changed() - func get_selected_asset_id() -> int: - # "Add new" is the final entry only when search box is blank - var max_id: int = max(0, entries.size() - (1 if search_text else 2)) - var id: int = clamp(selected_id, 0, max_id) - if plugin.debug: - print("Terrain3DListContainer ", name, ": get_selected_asset_id: selected_id: ", selected_id, ", clamped: ", id, ", entries: ", entries.size()) - if id >= entries.size(): - return 0 - var res: Resource = entries[id].resource - if not res: - return 0 - if type == Terrain3DAssets.AssetType.TYPE_MESH: - return (res as Terrain3DMeshAsset).id - else: - return (res as Terrain3DTextureAsset).id - - func _on_resource_inspected(p_resource: Resource) -> void: await get_tree().process_frame EditorInterface.edit_resource(p_resource) @@ -727,7 +724,7 @@ class ListContainer extends Container: class ListEntry extends MarginContainer: signal hovered() - signal clicked() + signal clicked(mouse_button: MouseButton) signal changed(resource: Resource) signal inspected(resource: Resource) @@ -984,7 +981,7 @@ class ListEntry extends MarginContainer: set_edited_resource(Terrain3DMeshAsset.new(), false) _on_edit() else: - emit_signal("clicked") + clicked.emit(MOUSE_BUTTON_LEFT) MOUSE_BUTTON_RIGHT: if resource: _on_edit() @@ -1025,7 +1022,7 @@ class ListEntry extends MarginContainer: if resource is Terrain3DMeshAsset: res.id = resource.id set_edited_resource(res, false) - emit_signal("clicked") + clicked.emit(MOUSE_BUTTON_RIGHT) emit_signal("inspected", resource) @@ -1072,7 +1069,7 @@ class ListEntry extends MarginContainer: func _on_edit() -> void: - emit_signal("clicked") + clicked.emit(MOUSE_BUTTON_RIGHT) emit_signal("inspected", resource) diff --git a/project/addons/terrain_3d/src/ui.gd b/project/addons/terrain_3d/src/ui.gd index da9deefea..2ed6fc6a0 100644 --- a/project/addons/terrain_3d/src/ui.gd +++ b/project/addons/terrain_3d/src/ui.gd @@ -285,9 +285,9 @@ func _on_setting_changed(p_setting: Variant = null) -> void: if not plugin.asset_dock: # Skip function if not _ready() return brush_data = tool_settings.get_settings() - brush_data["asset_id"] = plugin.asset_dock.current_list.get_selected_asset_id() + brush_data["asset_ids"] = plugin.asset_dock.current_list.selected_asset_ids if plugin.debug: - print("Terrain3DUI: _on_setting_changed: selected resource ID: ", brush_data["asset_id"]) + print("Terrain3DUI: _on_setting_changed: selected resource ID: ", brush_data["asset_ids"]) if plugin.editor: plugin.editor.set_brush_data(brush_data) inverted_input = brush_data.get("invert", false) diff --git a/src/terrain_3d_editor.cpp b/src/terrain_3d_editor.cpp index 6c85a1ae2..2080b656f 100644 --- a/src/terrain_3d_editor.cpp +++ b/src/terrain_3d_editor.cpp @@ -139,7 +139,7 @@ void Terrain3DEditor::_operate_map(const Vector3 &p_global_position, const real_ bool enable_texture = _brush_data["enable_texture"]; bool texture_filter = _brush_data["texture_filter"]; int margin = _brush_data["margin"]; - int asset_id = _brush_data["asset_id"]; + Array asset_ids = _brush_data["asset_ids"]; Vector2 slope_range = _brush_data["slope"]; bool enable_angle = _brush_data["enable_angle"]; @@ -326,6 +326,10 @@ void Terrain3DEditor::_operate_map(const Vector3 &p_global_position, const real_ if (!data->is_in_slope(brush_global_position, slope_range)) { continue; } + if (!asset_ids.size()) { + continue; + } + const int asset_id = asset_ids[0]; switch (_operation) { // Base Paint case REPLACE: { @@ -487,6 +491,10 @@ void Terrain3DEditor::_operate_map(const Vector3 &p_global_position, const real_ if (!cmap) { continue; } + if (!asset_ids.size()) { + continue; + } + const int asset_id = asset_ids[0]; float src_ctrl = cmap->get_pixelv(map_pixel_position).r; // Must be float int tex_id = (get_blend(src_ctrl) > 110 - margin) ? get_overlay(src_ctrl) : get_base(src_ctrl); if (tex_id != asset_id) { @@ -872,7 +880,6 @@ void Terrain3DEditor::set_brush_data(const Dictionary &p_data) { _brush_data["enable_texture"] = p_data.get("enable_texture", true); _brush_data["texture_filter"] = p_data.get("texture_filter", false); - _brush_data["asset_id"] = CLAMP(int(p_data.get("asset_id", 0)), 0, ((_tool == INSTANCER) ? Terrain3DAssets::MAX_MESHES : Terrain3DAssets::MAX_TEXTURES) - 1); _brush_data["margin"] = CLAMP(int(p_data.get("margin", 0)), -100, 100); _brush_data["enable_angle"] = p_data.get("enable_angle", true); @@ -888,6 +895,32 @@ void Terrain3DEditor::set_brush_data(const Dictionary &p_data) { _brush_data["brush_spin_speed"] = CLAMP(real_t(p_data.get("brush_spin_speed", 0.f)), 0.f, 1.f); _brush_data["gradient_points"] = p_data.get("gradient_points", PackedVector3Array()); + // Get and sanitize asset ids. If invalid, remove from list and log error + TypedArray asset_ids = p_data.get("asset_ids", TypedArray()); + + if (_tool == Tool::TEXTURE) { + TypedArray valid_ids; + int max_asset_id = -1; + if (_tool == Tool::TEXTURE) { + max_asset_id = _terrain->get_assets()->get_texture_count(); + } else if (_tool == Tool::INSTANCER) { + max_asset_id = _terrain->get_assets()->get_mesh_count(); + } + for (const int id : asset_ids) { + if (id < max_asset_id) { + valid_ids.push_back(id); + } else { + LOG(ERROR, "Brush data contains invalid asset id: ", id, ". Max asset id is: ", max_asset_id - 1); + } + } + asset_ids = valid_ids; + } + // Default to first asset id if none provided + if (asset_ids.is_empty()) { + asset_ids.push_back(0); + } + _brush_data["asset_ids"] = asset_ids; + Util::print_dict("set_brush_data() Santized brush data:", _brush_data, EXTREME); } diff --git a/src/terrain_3d_instancer.cpp b/src/terrain_3d_instancer.cpp index 182242781..a3b2c57d4 100644 --- a/src/terrain_3d_instancer.cpp +++ b/src/terrain_3d_instancer.cpp @@ -627,130 +627,162 @@ void Terrain3DInstancer::set_mode(const InstancerMode p_mode) { void Terrain3DInstancer::add_instances(const Vector3 &p_global_position, const Dictionary &p_params) { IS_DATA_INIT_MESG("Instancer isn't initialized.", VOID); - int mesh_id = p_params.get("asset_id", 0); - if (mesh_id < 0 || mesh_id >= _terrain->get_assets()->get_mesh_count()) { - LOG(ERROR, "Mesh ID out of range: ", mesh_id, ", valid: 0 to ", _terrain->get_assets()->get_mesh_count() - 1); + if (!p_params.has("asset_ids")) { + LOG(ERROR, "We need an array of asset_ids"); return; } real_t brush_size = CLAMP(real_t(p_params.get("size", 10.f)), 0.1f, 4096.f); // Meters real_t radius = brush_size * .5f; real_t strength = CLAMP(real_t(p_params.get("strength", .1f)), .01f, 100.f); // (premul) 1-10k% - real_t fixed_scale = CLAMP(real_t(p_params.get("fixed_scale", 100.f)) * .01f, .01f, 100.f); // 1-10k% - real_t random_scale = CLAMP(real_t(p_params.get("random_scale", 0.f)) * .01f, 0.f, 10.f); // +/- 1000% - Ref mesh_asset = _terrain->get_assets()->get_mesh_asset(mesh_id); - real_t density = CLAMP(.1f * brush_size * strength * mesh_asset->get_density() / - MAX(0.01f, fixed_scale + .5f * random_scale), - .001f, 1000.f); - // Density based on strength, mesh AABB and input scale determines how many to place, even fractional - uint32_t count = _get_density_count(density); - if (count <= 0) { - return; + + Array mesh_ids = p_params.get("asset_ids", Array()); + + if (_density_counters.size() != mesh_ids.size()) { + _density_counters.resize(mesh_ids.size(), 0); } - LOG(EXTREME, "Adding ", count, " instances at ", p_global_position); - real_t fixed_spin = CLAMP(real_t(p_params.get("fixed_spin", 0.f)), .0f, 360.f); // degrees - real_t random_spin = CLAMP(real_t(p_params.get("random_spin", 360.f)), 0.f, 360.f); // degrees - real_t fixed_tilt = CLAMP(real_t(p_params.get("fixed_tilt", 0.f)), -180.f, 180.f); // degrees - real_t random_tilt = CLAMP(real_t(p_params.get("random_tilt", 10.f)), 0.f, 180.f); // degrees - bool align_to_normal = bool(p_params.get("align_to_normal", false)); + for (int i = 0; i < mesh_ids.size(); i++) { + int mesh_id = mesh_ids[i]; + if (mesh_id < 0 || mesh_id >= _terrain->get_assets()->get_mesh_count()) { + LOG(ERROR, "Mesh ID out of range: ", mesh_id, ", valid: 0 to ", _terrain->get_assets()->get_mesh_count() - 1); + return; + } - real_t height_offset = CLAMP(real_t(p_params.get("height_offset", 0.f)), -100.0f, 100.f); // meters - real_t random_height = CLAMP(real_t(p_params.get("random_height", 0.f)), 0.f, 100.f); // meters + real_t fixed_scale = CLAMP(real_t(p_params.get("fixed_scale", 100.f)) * .01f, .01f, 100.f); // 1-10k% + real_t random_scale = CLAMP(real_t(p_params.get("random_scale", 0.f)) * .01f, 0.f, 10.f); // +/- 1000% + Ref mesh_asset = _terrain->get_assets()->get_mesh_asset(mesh_id); + real_t density = CLAMP(.1f * brush_size * strength * mesh_asset->get_density() / + MAX(0.01f, fixed_scale + .5f * random_scale), + .001f, 1000.f); + // Density based on strength, mesh AABB and input scale determines how many to place, even fractional + uint32_t count = _update_density_count(i, density); + if (count <= 0) { + return; + } + LOG(EXTREME, "Adding ", count, " instances at ", p_global_position); - Color vertex_color = Color(p_params.get("vertex_color", COLOR_WHITE)); - real_t random_hue = CLAMP(real_t(p_params.get("random_hue", 0.f)) / 360.f, 0.f, 1.f); // degrees -> 0-1 - real_t random_darken = CLAMP(real_t(p_params.get("random_darken", 0.f)) * .01f, 0.f, 1.f); // 0-100% + real_t fixed_spin = CLAMP(real_t(p_params.get("fixed_spin", 0.f)), .0f, 360.f); // degrees + real_t random_spin = CLAMP(real_t(p_params.get("random_spin", 360.f)), 0.f, 360.f); // degrees + real_t fixed_tilt = CLAMP(real_t(p_params.get("fixed_tilt", 0.f)), -180.f, 180.f); // degrees + real_t random_tilt = CLAMP(real_t(p_params.get("random_tilt", 10.f)), 0.f, 180.f); // degrees + bool align_to_normal = bool(p_params.get("align_to_normal", false)); - Vector2 slope_range = p_params.get("slope", Vector2(0.f, 90.f)); // 0-90 degrees - slope_range.x = CLAMP(slope_range.x, 0.f, 90.f); - slope_range.y = CLAMP(slope_range.y, 0.f, 90.f); - bool on_collision = bool(p_params.get("on_collision", false)); - real_t raycast_height = p_params.get("raycast_height", 10.f); - Terrain3DData *data = _terrain->get_data(); + real_t height_offset = CLAMP(real_t(p_params.get("height_offset", 0.f)), -100.0f, 100.f); // meters + real_t random_height = CLAMP(real_t(p_params.get("random_height", 0.f)), 0.f, 100.f); // meters - TypedArray xforms; - PackedColorArray colors; - for (int i = 0; i < count; i++) { - Transform3D t; - - // Get random XZ position and height in a circle - real_t r_radius = radius * sqrt(UtilityFunctions::randf()); - real_t r_theta = UtilityFunctions::randf() * Math_TAU; - Vector3 rand_vec = Vector3(r_radius * cos(r_theta), 0.f, r_radius * sin(r_theta)); - Vector3 position = p_global_position + rand_vec; - - // Get height - Array height_data = _get_usable_height(position, slope_range, on_collision, raycast_height); - if (height_data.size() != 3) { - continue; - } - position.y = height_data[0]; - bool raycast_hit = height_data[1]; - - // Orientation - Vector3 normal = V3_UP; - if (align_to_normal) { - // Use either collision normal or terrain normal - normal = raycast_hit ? (Vector3)height_data[2] : data->get_normal(position); - if (!normal.is_finite()) { - normal = V3_UP; - } else { - normal = normal.normalized(); - Vector3 z_axis = Vector3(0.f, 0.f, 1.f); - Vector3 x_axis = -z_axis.cross(normal); - if (x_axis.length_squared() > 0.001f) { - t.basis = Basis(x_axis, normal, z_axis).orthonormalized(); + Color vertex_color = Color(p_params.get("vertex_color", COLOR_WHITE)); + real_t random_hue = CLAMP(real_t(p_params.get("random_hue", 0.f)) / 360.f, 0.f, 1.f); // degrees -> 0-1 + real_t random_darken = CLAMP(real_t(p_params.get("random_darken", 0.f)) * .01f, 0.f, 1.f); // 0-100% + + Vector2 slope_range = p_params.get("slope", Vector2(0.f, 90.f)); // 0-90 degrees + slope_range.x = CLAMP(slope_range.x, 0.f, 90.f); + slope_range.y = CLAMP(slope_range.y, 0.f, 90.f); + bool on_collision = bool(p_params.get("on_collision", false)); + real_t raycast_height = p_params.get("raycast_height", 10.f); + Terrain3DData *data = _terrain->get_data(); + + TypedArray xforms; + PackedColorArray colors; + for (int i = 0; i < count; i++) { + Transform3D t; + + // Get random XZ position and height in a circle + real_t r_radius = radius * sqrt(UtilityFunctions::randf()); + real_t r_theta = UtilityFunctions::randf() * Math_TAU; + Vector3 rand_vec = Vector3(r_radius * cos(r_theta), 0.f, r_radius * sin(r_theta)); + Vector3 position = p_global_position + rand_vec; + + // Get height + Array height_data = _get_usable_height(position, slope_range, on_collision, raycast_height); + if (height_data.size() != 3) { + continue; + } + position.y = height_data[0]; + bool raycast_hit = height_data[1]; + + // Orientation + Vector3 normal = V3_UP; + if (align_to_normal) { + // Use either collision normal or terrain normal + normal = raycast_hit ? (Vector3)height_data[2] : data->get_normal(position); + if (!normal.is_finite()) { + normal = V3_UP; + } else { + normal = normal.normalized(); + Vector3 z_axis = Vector3(0.f, 0.f, 1.f); + Vector3 x_axis = -z_axis.cross(normal); + if (x_axis.length_squared() > 0.001f) { + t.basis = Basis(x_axis, normal, z_axis).orthonormalized(); + } } } - } - real_t spin = (fixed_spin + random_spin * UtilityFunctions::randf()) * Math_PI / 180.f; - if (std::abs(spin) > 0.001f) { - t.basis = t.basis.rotated(normal, spin); - } - real_t tilt = (fixed_tilt + random_tilt * (2.f * UtilityFunctions::randf() - 1.f)) * Math_PI / 180.f; - if (std::abs(tilt) > 0.001f) { - t.basis = t.basis.rotated(t.basis.get_column(0), tilt); // Rotate pitch, X-axis - } + real_t spin = (fixed_spin + random_spin * UtilityFunctions::randf()) * Math_PI / 180.f; + if (std::abs(spin) > 0.001f) { + t.basis = t.basis.rotated(normal, spin); + } + real_t tilt = (fixed_tilt + random_tilt * (2.f * UtilityFunctions::randf() - 1.f)) * Math_PI / 180.f; + if (std::abs(tilt) > 0.001f) { + t.basis = t.basis.rotated(t.basis.get_column(0), tilt); // Rotate pitch, X-axis + } - // Scale - 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)); + // Scale + 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. 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); + // 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); - // Color - Color col = vertex_color; - col.set_v(CLAMP(col.get_v() - random_darken * UtilityFunctions::randf(), 0.f, 1.f)); - col.set_h(fmod(col.get_h() + random_hue * (2.f * UtilityFunctions::randf() - 1.f), 1.f)); + // Color + Color col = vertex_color; + col.set_v(CLAMP(col.get_v() - random_darken * UtilityFunctions::randf(), 0.f, 1.f)); + col.set_h(fmod(col.get_h() + random_hue * (2.f * UtilityFunctions::randf() - 1.f), 1.f)); - xforms.push_back(t); - colors.push_back(col); - } + xforms.push_back(t); + colors.push_back(col); + } - // Append multimesh - if (xforms.size() > 0) { - add_transforms(mesh_id, xforms, colors); + // Append multimesh + if (xforms.size() > 0) { + add_transforms(mesh_id, xforms, colors); + } } } void Terrain3DInstancer::remove_instances(const Vector3 &p_global_position, const Dictionary &p_params) { IS_DATA_INIT_MESG("Instancer isn't initialized.", VOID); - - int mesh_id = p_params.get("asset_id", 0); + if (!p_params.has("asset_ids")) { + LOG(ERROR, "We need an array of asset_ids"); + return; + } + Array mesh_ids = p_params.get("asset_ids", Array()); int mesh_count = _terrain->get_assets()->get_mesh_count(); - if (mesh_id < 0 || mesh_id >= mesh_count) { - LOG(ERROR, "Mesh ID out of range: ", mesh_id, ", valid: 0 to ", _terrain->get_assets()->get_mesh_count() - 1); + + bool modifier_shift = p_params.get("modifier_shift", false); + if (modifier_shift) { + mesh_ids.resize(mesh_count); + for (int i = 0; i < mesh_count; i++) { + mesh_ids[i] = i; + } + } + + if (mesh_ids.is_empty()) { + LOG(ERROR, "We need an array of asset_ids"); return; } + for (const int mesh_id : mesh_ids) { + if (mesh_id < 0 || mesh_id >= mesh_count) { + LOG(ERROR, "Mesh ID out of range: ", mesh_id, ", valid: 0 to ", mesh_count - 1); + return; + } + } + Terrain3DData *data = _terrain->get_data(); int region_size = _terrain->get_region_size(); real_t vertex_spacing = _terrain->get_vertex_spacing(); - bool modifier_shift = p_params.get("modifier_shift", false); real_t brush_size = CLAMP(real_t(p_params.get("size", 10.f)), .5f, 4096.f); // Meters real_t half_brush_size = brush_size * 0.5f + 1.f; // 1m margin real_t radius = brush_size * .5f; @@ -794,17 +826,17 @@ void Terrain3DInstancer::remove_instances(const Vector3 &p_global_position, cons } Vector3 global_local_offset = Vector3(region_loc.x * region_size * vertex_spacing, 0.f, region_loc.y * region_size * vertex_spacing); Vector2 localised_ring_center = Vector2(p_global_position.x - global_local_offset.x, p_global_position.z - global_local_offset.z); - // For this mesh id, or all mesh ids - for (int m = (modifier_shift ? 0 : mesh_id); m <= (modifier_shift ? mesh_count - 1 : mesh_id); m++) { + + for (const int mesh_id : mesh_ids) { // Ensure this region has this mesh - if (!mesh_inst_dict.has(m)) { + if (!mesh_inst_dict.has(mesh_id)) { continue; } - Dictionary cell_inst_dict = mesh_inst_dict[m]; + Dictionary cell_inst_dict = mesh_inst_dict[mesh_id]; Array cell_locations = cell_inst_dict.keys(); // This shouldnt be empty if (cell_locations.size() == 0) { - LOG(WARN, "Region at: ", region_loc, " has instance dictionary for mesh id: ", m, " but has no cells.") + LOG(WARN, "Region at: ", region_loc, " has instance dictionary for mesh id: ", mesh_id, " but has no cells.") continue; } // Check potential cells rather than searching the entire region, whilst marginally @@ -829,7 +861,7 @@ void Terrain3DInstancer::remove_instances(const Vector3 &p_global_position, cons if (cell_queue.size() == 0) { continue; } - Ref mesh_asset = _terrain->get_assets()->get_mesh_asset(m); + Ref mesh_asset = _terrain->get_assets()->get_mesh_asset(mesh_id); real_t mesh_height_offset = mesh_asset->get_height_offset(); for (int c = 0; c < cell_queue.size(); c++) { Vector2i cell = cell_queue[c]; @@ -865,13 +897,13 @@ void Terrain3DInstancer::remove_instances(const Vector3 &p_global_position, cons cell_inst_dict[cell] = triple; } else { cell_inst_dict.erase(cell); - _destroy_mmi_by_cell(region_loc, m, cell); + _destroy_mmi_by_cell(region_loc, mesh_id, cell); } } if (cell_inst_dict.is_empty()) { - mesh_inst_dict.erase(m); + mesh_inst_dict.erase(mesh_id); } - update_mmis(m, region_loc); + update_mmis(mesh_id, region_loc); } } } diff --git a/src/terrain_3d_instancer.h b/src/terrain_3d_instancer.h index 5ea3ed047..28651d714 100644 --- a/src/terrain_3d_instancer.h +++ b/src/terrain_3d_instancer.h @@ -50,9 +50,9 @@ class Terrain3DInstancer : public Object { V2IIntPair _queued_updates; InstancerMode _mode = NORMAL; - uint32_t _density_counter = 0; + std::vector _density_counters; - uint32_t _get_density_count(const real_t p_density); + uint32_t _update_density_count(const uint32_t p_id, const real_t p_density); int _get_master_lod(const Ref &p_ma); void _process_updates(); void _update_mmi_by_region(const Terrain3DRegion *p_region, const int p_mesh_id); @@ -96,7 +96,7 @@ class Terrain3DInstancer : public Object { void swap_ids(const int p_src_id, const int p_dst_id); void update_mmis(const int p_mesh_id = -1, const Vector2i &p_region_loc = V2I_MAX, const bool p_rebuild = false); - void reset_density_counter() { _density_counter = 0; } + void reset_density_counter() { _density_counters.clear(); } protected: static void _bind_methods(); @@ -107,14 +107,16 @@ VARIANT_ENUM_CAST(Terrain3DInstancer::InstancerMode); // Allows us to instance every X function calls for sparse placement // Modifies _density_counter, not const! -inline uint32_t Terrain3DInstancer::_get_density_count(const real_t p_density) { - uint32_t count = 0; - if (p_density < 1.f && _density_counter++ % uint32_t(1.f / p_density) == 0) { - count = 1; - } else if (p_density >= 1.f) { - count = uint32_t(p_density); +inline uint32_t Terrain3DInstancer::_update_density_count(const uint32_t p_id, const real_t p_density) { + if (p_id >= _density_counters.size()) { + _density_counters.resize(p_id + 1, 0); } - return count; + if (p_density < 1.f) { + uint32_t &counter = _density_counters[p_id]; + const uint32_t interval = uint32_t(1.f / p_density); + return uint32_t(++counter % interval == 0); + } + return uint32_t(p_density); } // Use lod0 to track instance counter and set AABB, but in shadows_only lod0 doesn't exist