diff options
author | Gone2Daly <71726742+Gone2Daly@users.noreply.github.com> | 2023-07-22 21:05:42 +0200 |
---|---|---|
committer | Gone2Daly <71726742+Gone2Daly@users.noreply.github.com> | 2023-07-22 21:05:42 +0200 |
commit | 71b3cd829f80de4c2cd3972d8bfd5ee470a5d180 (patch) | |
tree | b4280fde6eef2ae6987648bc7bf8e00e9011bb7f /game/addons/zylann.hterrain/tools/brush | |
parent | ce9022d0df74d6c33db3686622be2050d873ab0b (diff) |
init_testtest3d
Diffstat (limited to 'game/addons/zylann.hterrain/tools/brush')
42 files changed, 3167 insertions, 0 deletions
diff --git a/game/addons/zylann.hterrain/tools/brush/brush.gd b/game/addons/zylann.hterrain/tools/brush/brush.gd new file mode 100644 index 0000000..73d447f --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/brush.gd @@ -0,0 +1,217 @@ +@tool + +# Brush properties (shape, transform, timing and opacity). +# Other attributes like color, height or texture index are tool-specific, +# while brush properties apply to all of them. +# This is separate from Painter because it could apply to multiple Painters at once. + +const HT_Errors = preload("../../util/errors.gd") +const HT_Painter = preload("./painter.gd") + +const SHAPES_DIR = "addons/zylann.hterrain/tools/brush/shapes" +const DEFAULT_BRUSH_TEXTURE_PATH = SHAPES_DIR + "/round2.exr" +# Reasonable size for sliders to be usable +const MAX_SIZE_FOR_SLIDERS = 500 +# Absolute size limit. Terrains can't be larger than that, and it will be very slow to paint +const MAX_SIZE = 4000 + +signal size_changed(new_size) +signal shapes_changed +signal shape_index_changed + +var _size := 32 +var _opacity := 1.0 +var _random_rotation := false +var _pressure_enabled := false +var _pressure_over_scale := 0.5 +var _pressure_over_opacity := 0.5 +# TODO Rename stamp_*? +var _frequency_distance := 0.0 +var _frequency_time_ms := 0 +# Array of greyscale textures +var _shapes : Array[Texture2D] = [] + +var _shape_index := 0 +var _shape_cycling_enabled := false +var _prev_position := Vector2(-999, -999) +var _prev_time_ms := 0 + + +func set_size(size: int): + if size < 1: + size = 1 + if size != _size: + _size = size + size_changed.emit(_size) + + +func get_size() -> int: + return _size + + +func set_opacity(opacity: float): + _opacity = clampf(opacity, 0.0, 1.0) + + +func get_opacity() -> float: + return _opacity + + +func set_random_rotation_enabled(enabled: bool): + _random_rotation = enabled + + +func is_random_rotation_enabled() -> bool: + return _random_rotation + + +func set_pressure_enabled(enabled: bool): + _pressure_enabled = enabled + + +func is_pressure_enabled() -> bool: + return _pressure_enabled + + +func set_pressure_over_scale(amount: float): + _pressure_over_scale = clampf(amount, 0.0, 1.0) + + +func get_pressure_over_scale() -> float: + return _pressure_over_scale + + +func set_pressure_over_opacity(amount: float): + _pressure_over_opacity = clampf(amount, 0.0, 1.0) + + +func get_pressure_over_opacity() -> float: + return _pressure_over_opacity + + +func set_frequency_distance(d: float): + _frequency_distance = maxf(d, 0.0) + + +func get_frequency_distance() -> float: + return _frequency_distance + + +func set_frequency_time_ms(t: int): + if t < 0: + t = 0 + _frequency_time_ms = t + + +func get_frequency_time_ms() -> int: + return _frequency_time_ms + + +func set_shapes(shapes: Array[Texture2D]): + assert(len(shapes) >= 1) + for s in shapes: + assert(s != null) + assert(s is Texture2D) + _shapes = shapes.duplicate(false) + if _shape_index >= len(_shapes): + _shape_index = len(_shapes) - 1 + shapes_changed.emit() + + +func get_shapes() -> Array[Texture2D]: + return _shapes.duplicate(false) + + +func get_shape(i: int) -> Texture2D: + return _shapes[i] + + +func get_shape_index() -> int: + return _shape_index + + +func set_shape_index(i: int): + assert(i >= 0) + assert(i < len(_shapes)) + _shape_index = i + shape_index_changed.emit() + + +func set_shape_cycling_enabled(enable: bool): + _shape_cycling_enabled = enable + + +func is_shape_cycling_enabled() -> bool: + return _shape_cycling_enabled + + +static func load_shape_from_image_file(fpath: String, logger, retries := 1) -> Texture2D: + var im := Image.new() + var err := im.load(fpath) + if err != OK: + if retries > 0: + # TODO There is a bug with Godot randomly being unable to load images. + # See https://github.com/Zylann/godot_heightmap_plugin/issues/219 + # Attempting to workaround this by retrying (I suspect it's because of non-initialized + # variable in Godot's C++ code...) + logger.error("Could not load image at '{0}', error {1}. Retrying..." \ + .format([fpath, HT_Errors.get_message(err)])) + return load_shape_from_image_file(fpath, logger, retries - 1) + else: + logger.error("Could not load image at '{0}', error {1}" \ + .format([fpath, HT_Errors.get_message(err)])) + return null + var tex := ImageTexture.create_from_image(im) + return tex + + +# Call this while handling mouse or pen input. +# If it returns false, painting should not run. +func configure_paint_input(painters: Array[HT_Painter], position: Vector2, pressure: float) -> bool: + assert(len(_shapes) != 0) + + # DEBUG + #pressure = 0.5 + 0.5 * sin(OS.get_ticks_msec() / 200.0) + + if position.distance_to(_prev_position) < _frequency_distance: + return false + var now := Time.get_ticks_msec() + if (now - _prev_time_ms) < _frequency_time_ms: + return false + _prev_position = position + _prev_time_ms = now + + for painter_index in len(painters): + var painter : HT_Painter = painters[painter_index] + + if _random_rotation: + painter.set_brush_rotation(randf_range(-PI, PI)) + else: + painter.set_brush_rotation(0.0) + + painter.set_brush_texture(_shapes[_shape_index]) + painter.set_brush_size(_size) + + if _pressure_enabled: + painter.set_brush_scale(lerpf(1.0, pressure, _pressure_over_scale)) + painter.set_brush_opacity(_opacity * lerpf(1.0, pressure, _pressure_over_opacity)) + else: + painter.set_brush_scale(1.0) + painter.set_brush_opacity(_opacity) + + #painter.paint_input(position) + + if _shape_cycling_enabled: + _shape_index += 1 + if _shape_index >= len(_shapes): + _shape_index = 0 + + return true + + +# Call this when the user releases the pen or mouse button +func on_paint_end(): + _prev_position = Vector2(-999, -999) + _prev_time_ms = 0 + + diff --git a/game/addons/zylann.hterrain/tools/brush/brush_editor.gd b/game/addons/zylann.hterrain/tools/brush/brush_editor.gd new file mode 100644 index 0000000..1e645a3 --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/brush_editor.gd @@ -0,0 +1,234 @@ +@tool +extends Control + +const HT_TerrainPainter = preload("./terrain_painter.gd") +const HT_Brush = preload("./brush.gd") +const HT_Errors = preload("../../util/errors.gd") +#const NativeFactory = preload("../../native/factory.gd") +const HT_Logger = preload("../../util/logger.gd") +const HT_IntervalSlider = preload("../util/interval_slider.gd") + +const HT_BrushSettingsDialogScene = preload("./settings_dialog/brush_settings_dialog.tscn") +const HT_BrushSettingsDialog = preload("./settings_dialog/brush_settings_dialog.gd") + + +@onready var _size_slider : Slider = $GridContainer/BrushSizeControl/Slider +@onready var _size_value_label : Label = $GridContainer/BrushSizeControl/Label +#onready var _size_label = _params_container.get_node("BrushSizeLabel") + +@onready var _opacity_slider : Slider = $GridContainer/BrushOpacityControl/Slider +@onready var _opacity_value_label : Label = $GridContainer/BrushOpacityControl/Label +@onready var _opacity_control : Control = $GridContainer/BrushOpacityControl +@onready var _opacity_label : Label = $GridContainer/BrushOpacityLabel + +@onready var _flatten_height_container : Control = $GridContainer/HB +@onready var _flatten_height_box : SpinBox = $GridContainer/HB/FlattenHeightControl +@onready var _flatten_height_label : Label = $GridContainer/FlattenHeightLabel +@onready var _flatten_height_pick_button : Button = $GridContainer/HB/FlattenHeightPickButton + +@onready var _color_picker : ColorPickerButton = $GridContainer/ColorPickerButton +@onready var _color_label : Label = $GridContainer/ColorLabel + +@onready var _density_slider : Slider = $GridContainer/DensitySlider +@onready var _density_label : Label = $GridContainer/DensityLabel + +@onready var _holes_label : Label = $GridContainer/HoleLabel +@onready var _holes_checkbox : CheckBox = $GridContainer/HoleCheckbox + +@onready var _slope_limit_label : Label = $GridContainer/SlopeLimitLabel +@onready var _slope_limit_control : HT_IntervalSlider = $GridContainer/SlopeLimit + +@onready var _shape_texture_rect : TextureRect = get_node("BrushShapeButton/TextureRect") + +var _terrain_painter : HT_TerrainPainter +var _brush_settings_dialog : HT_BrushSettingsDialog = null +var _logger = HT_Logger.get_for(self) + +# TODO This is an ugly workaround for https://github.com/godotengine/godot/issues/19479 +@onready var _temp_node = get_node("Temp") +@onready var _grid_container = get_node("GridContainer") +func _set_visibility_of(node: Control, v: bool): + node.get_parent().remove_child(node) + if v: + _grid_container.add_child(node) + else: + _temp_node.add_child(node) + node.visible = v + + +func _ready(): + _size_slider.value_changed.connect(_on_size_slider_value_changed) + _opacity_slider.value_changed.connect(_on_opacity_slider_value_changed) + _flatten_height_box.value_changed.connect(_on_flatten_height_box_value_changed) + _color_picker.color_changed.connect(_on_color_picker_color_changed) + _density_slider.value_changed.connect(_on_density_slider_changed) + _holes_checkbox.toggled.connect(_on_holes_checkbox_toggled) + _slope_limit_control.changed.connect(_on_slope_limit_changed) + + _size_slider.max_value = HT_Brush.MAX_SIZE_FOR_SLIDERS + #if NativeFactory.is_native_available(): + # _size_slider.max_value = 200 + #else: + # _size_slider.max_value = 50 + + +func setup_dialogs(base_control: Node): + assert(_brush_settings_dialog == null) + _brush_settings_dialog = HT_BrushSettingsDialogScene.instantiate() + base_control.add_child(_brush_settings_dialog) + + # That dialog has sub-dialogs + _brush_settings_dialog.setup_dialogs(base_control) + _brush_settings_dialog.set_brush(_terrain_painter.get_brush()) + + +func _exit_tree(): + if _brush_settings_dialog != null: + _brush_settings_dialog.queue_free() + _brush_settings_dialog = null + +# Testing display modes +#var mode = 0 +#func _input(event): +# if event is InputEventKey: +# if event.pressed: +# set_display_mode(mode) +# mode += 1 +# if mode >= Brush.MODE_COUNT: +# mode = 0 + +func set_terrain_painter(terrain_painter: HT_TerrainPainter): + if _terrain_painter != null: + _terrain_painter.flatten_height_changed.disconnect(_on_flatten_height_changed) + _terrain_painter.get_brush().shapes_changed.disconnect(_on_brush_shapes_changed) + _terrain_painter.get_brush().shape_index_changed.disconnect(_on_brush_shape_index_changed) + + _terrain_painter = terrain_painter + + if _terrain_painter != null: + # TODO Had an issue in Godot 3.2.3 where mismatching type would silently cast to null... + # It happens if the argument went through a Variant (for example if call_deferred is used) + assert(_terrain_painter != null) + + if _terrain_painter != null: + # Initial brush params + _size_slider.value = _terrain_painter.get_brush().get_size() + _opacity_slider.ratio = _terrain_painter.get_brush().get_opacity() + # Initial specific params + _flatten_height_box.value = _terrain_painter.get_flatten_height() + _color_picker.get_picker().color = _terrain_painter.get_color() + _density_slider.value = _terrain_painter.get_detail_density() + _holes_checkbox.button_pressed = not _terrain_painter.get_mask_flag() + + var low := rad_to_deg(_terrain_painter.get_slope_limit_low_angle()) + var high := rad_to_deg(_terrain_painter.get_slope_limit_high_angle()) + _slope_limit_control.set_values(low, high) + + set_display_mode(_terrain_painter.get_mode()) + + # Load default brush + var brush := _terrain_painter.get_brush() + var default_shape_fpath := HT_Brush.DEFAULT_BRUSH_TEXTURE_PATH + var default_shape := HT_Brush.load_shape_from_image_file(default_shape_fpath, _logger) + brush.set_shapes([default_shape]) + _update_shape_preview() + + _terrain_painter.flatten_height_changed.connect(_on_flatten_height_changed) + brush.shapes_changed.connect(_on_brush_shapes_changed) + brush.shape_index_changed.connect(_on_brush_shape_index_changed) + + +func _on_flatten_height_changed(): + _flatten_height_box.value = _terrain_painter.get_flatten_height() + _flatten_height_pick_button.button_pressed = false + + +func _on_brush_shapes_changed(): + _update_shape_preview() + + +func _on_brush_shape_index_changed(): + _update_shape_preview() + + +func _update_shape_preview(): + var brush := _terrain_painter.get_brush() + var i := brush.get_shape_index() + _shape_texture_rect.texture = brush.get_shape(i) + + +func set_display_mode(mode: int): + var show_flatten := mode == HT_TerrainPainter.MODE_FLATTEN + var show_color := mode == HT_TerrainPainter.MODE_COLOR + var show_density := mode == HT_TerrainPainter.MODE_DETAIL + var show_opacity := mode != HT_TerrainPainter.MODE_MASK + var show_holes := mode == HT_TerrainPainter.MODE_MASK + var show_slope_limit := \ + mode == HT_TerrainPainter.MODE_SPLAT or mode == HT_TerrainPainter.MODE_DETAIL + + _set_visibility_of(_opacity_label, show_opacity) + _set_visibility_of(_opacity_control, show_opacity) + + _set_visibility_of(_color_label, show_color) + _set_visibility_of(_color_picker, show_color) + + _set_visibility_of(_flatten_height_label, show_flatten) + _set_visibility_of(_flatten_height_container, show_flatten) + + _set_visibility_of(_density_label, show_density) + _set_visibility_of(_density_slider, show_density) + + _set_visibility_of(_holes_label, show_holes) + _set_visibility_of(_holes_checkbox, show_holes) + + _set_visibility_of(_slope_limit_label, show_slope_limit) + _set_visibility_of(_slope_limit_control, show_slope_limit) + + _flatten_height_pick_button.button_pressed = false + + +func _on_size_slider_value_changed(v: float): + if _terrain_painter != null: + _terrain_painter.set_brush_size(int(v)) + _size_value_label.text = str(v) + + +func _on_opacity_slider_value_changed(v: float): + if _terrain_painter != null: + _terrain_painter.set_opacity(_opacity_slider.ratio) + _opacity_value_label.text = str(v) + + +func _on_flatten_height_box_value_changed(v: float): + if _terrain_painter != null: + _terrain_painter.set_flatten_height(v) + + +func _on_color_picker_color_changed(v: Color): + if _terrain_painter != null: + _terrain_painter.set_color(v) + + +func _on_density_slider_changed(v: float): + if _terrain_painter != null: + _terrain_painter.set_detail_density(v) + + +func _on_holes_checkbox_toggled(v: bool): + if _terrain_painter != null: + # When checked, we draw holes. When unchecked, we clear holes + _terrain_painter.set_mask_flag(not v) + + +func _on_BrushShapeButton_pressed(): + _brush_settings_dialog.popup_centered() + + +func _on_FlattenHeightPickButton_pressed(): + _terrain_painter.set_meta("pick_height", true) + + +func _on_slope_limit_changed(): + var low = deg_to_rad(_slope_limit_control.get_low_value()) + var high = deg_to_rad(_slope_limit_control.get_high_value()) + _terrain_painter.set_slope_limit_angles(low, high) diff --git a/game/addons/zylann.hterrain/tools/brush/brush_editor.tscn b/game/addons/zylann.hterrain/tools/brush/brush_editor.tscn new file mode 100644 index 0000000..42ef980 --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/brush_editor.tscn @@ -0,0 +1,130 @@ +[gd_scene load_steps=4 format=3 uid="uid://bd42ig216p216"] + +[ext_resource type="Script" path="res://addons/zylann.hterrain/tools/brush/brush_editor.gd" id="1"] +[ext_resource type="Script" path="res://addons/zylann.hterrain/tools/util/interval_slider.gd" id="3"] + +[sub_resource type="CanvasItemMaterial" id="1"] +blend_mode = 1 + +[node name="BrushEditor" type="HBoxContainer"] +custom_minimum_size = Vector2(200, 0) +offset_right = 293.0 +offset_bottom = 211.0 +script = ExtResource("1") + +[node name="BrushShapeButton" type="Button" parent="."] +custom_minimum_size = Vector2(50, 0) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="BrushShapeButton"] +material = SubResource("1") +layout_mode = 0 +anchor_right = 1.0 +anchor_bottom = 1.0 +mouse_filter = 2 +expand_mode = 1 +stretch_mode = 5 + +[node name="GridContainer" type="GridContainer" parent="."] +layout_mode = 2 +size_flags_horizontal = 3 +columns = 2 + +[node name="BrushSizeLabel" type="Label" parent="GridContainer"] +layout_mode = 2 +text = "Brush size" + +[node name="BrushSizeControl" type="HBoxContainer" parent="GridContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +mouse_filter = 0 + +[node name="Slider" type="HSlider" parent="GridContainer/BrushSizeControl"] +custom_minimum_size = Vector2(60, 0) +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 1 +min_value = 2.0 +max_value = 500.0 +value = 2.0 +exp_edit = true +rounded = true + +[node name="Label" type="Label" parent="GridContainer/BrushSizeControl"] +layout_mode = 2 +text = "999" + +[node name="BrushOpacityLabel" type="Label" parent="GridContainer"] +layout_mode = 2 +text = "Brush opacity" + +[node name="BrushOpacityControl" type="HBoxContainer" parent="GridContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="Slider" type="HSlider" parent="GridContainer/BrushOpacityControl"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 1 + +[node name="Label" type="Label" parent="GridContainer/BrushOpacityControl"] +layout_mode = 2 +text = "999" + +[node name="FlattenHeightLabel" type="Label" parent="GridContainer"] +layout_mode = 2 +text = "Flatten height" + +[node name="HB" type="HBoxContainer" parent="GridContainer"] +layout_mode = 2 + +[node name="FlattenHeightControl" type="SpinBox" parent="GridContainer/HB"] +layout_mode = 2 +size_flags_horizontal = 3 +min_value = -500.0 +max_value = 500.0 +step = 0.01 + +[node name="FlattenHeightPickButton" type="Button" parent="GridContainer/HB"] +layout_mode = 2 +toggle_mode = true +text = "Pick" + +[node name="ColorLabel" type="Label" parent="GridContainer"] +layout_mode = 2 +text = "Color" + +[node name="ColorPickerButton" type="ColorPickerButton" parent="GridContainer"] +layout_mode = 2 +toggle_mode = false +color = Color(1, 1, 1, 1) + +[node name="DensityLabel" type="Label" parent="GridContainer"] +layout_mode = 2 +text = "Detail density" + +[node name="DensitySlider" type="HSlider" parent="GridContainer"] +layout_mode = 2 +max_value = 1.0 +step = 0.1 + +[node name="HoleLabel" type="Label" parent="GridContainer"] +layout_mode = 2 +text = "Draw holes" + +[node name="HoleCheckbox" type="CheckBox" parent="GridContainer"] +layout_mode = 2 + +[node name="SlopeLimitLabel" type="Label" parent="GridContainer"] +layout_mode = 2 +text = "Slope limit" + +[node name="SlopeLimit" type="Control" parent="GridContainer"] +layout_mode = 2 +script = ExtResource("3") +range = Vector2(0, 90) + +[node name="Temp" type="Node" parent="."] + +[connection signal="pressed" from="BrushShapeButton" to="." method="_on_BrushShapeButton_pressed"] +[connection signal="pressed" from="GridContainer/HB/FlattenHeightPickButton" to="." method="_on_FlattenHeightPickButton_pressed"] diff --git a/game/addons/zylann.hterrain/tools/brush/decal.gd b/game/addons/zylann.hterrain/tools/brush/decal.gd new file mode 100644 index 0000000..13433ed --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/decal.gd @@ -0,0 +1,121 @@ +@tool +# Shows a cursor on top of the terrain to preview where the brush will paint + +# TODO Use an actual decal node, it wasn't available in Godot 3 + +const HT_DirectMeshInstance = preload("../../util/direct_mesh_instance.gd") +const HTerrain = preload("../../hterrain.gd") +const HTerrainData = preload("../../hterrain_data.gd") +const HT_Util = preload("../../util/util.gd") + +var _mesh_instance : HT_DirectMeshInstance +var _mesh : PlaneMesh +var _material = ShaderMaterial.new() +#var _debug_mesh = CubeMesh.new() +#var _debug_mesh_instance = null + +var _terrain : HTerrain = null + + +func _init(): + _material.shader = load("res://addons/zylann.hterrain/tools/brush/decal.gdshader") + _mesh_instance = HT_DirectMeshInstance.new() + _mesh_instance.set_material(_material) + + _mesh = PlaneMesh.new() + _mesh_instance.set_mesh(_mesh) + + #_debug_mesh_instance = DirectMeshInstance.new() + #_debug_mesh_instance.set_mesh(_debug_mesh) + + +func set_size(size: float): + _mesh.size = Vector2(size, size) + # Must line up to terrain vertex policy, so must apply an off-by-one. + # If I don't do that, the brush will appear to wobble above the ground + var ss := size - 1 + # Don't subdivide too much + while ss > 50: + ss /= 2 + _mesh.subdivide_width = ss + _mesh.subdivide_depth = ss + + +#func set_shape(shape_image): +# set_size(shape_image.get_width()) + + +func _on_terrain_transform_changed(terrain_global_trans: Transform3D): + var inv = terrain_global_trans.affine_inverse() + _material.set_shader_parameter("u_terrain_inverse_transform", inv) + + var normal_basis = terrain_global_trans.basis.inverse().transposed() + _material.set_shader_parameter("u_terrain_normal_basis", normal_basis) + + +func set_terrain(terrain: HTerrain): + if _terrain == terrain: + return + + if _terrain != null: + _terrain.transform_changed.disconnect(_on_terrain_transform_changed) + _mesh_instance.exit_world() + #_debug_mesh_instance.exit_world() + + _terrain = terrain + + if _terrain != null: + _terrain.transform_changed.connect(_on_terrain_transform_changed) + _on_terrain_transform_changed(_terrain.get_internal_transform()) + _mesh_instance.enter_world(terrain.get_world_3d()) + #_debug_mesh_instance.enter_world(terrain.get_world()) + + update_visibility() + + +func set_position(p_local_pos: Vector3): + assert(_terrain != null) + assert(typeof(p_local_pos) == TYPE_VECTOR3) + + # Set custom AABB (in local cells) because the decal is displaced by shader + var data = _terrain.get_data() + if data != null: + var r = _mesh.size / 2 + var aabb = data.get_region_aabb( \ + int(p_local_pos.x - r.x), \ + int(p_local_pos.z - r.y), \ + int(2 * r.x), \ + int(2 * r.y)) + aabb.position = Vector3(-r.x, aabb.position.y, -r.y) + _mesh.custom_aabb = aabb + #_debug_mesh.size = aabb.size + + var trans = Transform3D(Basis(), p_local_pos) + var terrain_gt = _terrain.get_internal_transform() + trans = terrain_gt * trans + _mesh_instance.set_transform(trans) + #_debug_mesh_instance.set_transform(trans) + + +# This is called very often so it should be cheap +func update_visibility(): + var heightmap = _get_heightmap(_terrain) + if heightmap == null: + # I do this for refcounting because heightmaps are large resources + _material.set_shader_parameter("u_terrain_heightmap", null) + _mesh_instance.set_visible(false) + #_debug_mesh_instance.set_visible(false) + else: + _material.set_shader_parameter("u_terrain_heightmap", heightmap) + _mesh_instance.set_visible(true) + #_debug_mesh_instance.set_visible(true) + + +func _get_heightmap(terrain): + if terrain == null: + return null + var data = terrain.get_data() + if data == null: + return null + return data.get_texture(HTerrainData.CHANNEL_HEIGHT) + diff --git a/game/addons/zylann.hterrain/tools/brush/decal.gdshader b/game/addons/zylann.hterrain/tools/brush/decal.gdshader new file mode 100644 index 0000000..b56f43a --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/decal.gdshader @@ -0,0 +1,41 @@ +shader_type spatial; +render_mode unshaded;//, depth_test_disable; + +#include "res://addons/zylann.hterrain/shaders/include/heightmap.gdshaderinc" + +uniform sampler2D u_terrain_heightmap; +uniform mat4 u_terrain_inverse_transform; +uniform mat3 u_terrain_normal_basis; + +float get_height(sampler2D heightmap, vec2 uv) { + return sample_heightmap(heightmap, uv); +} + +void vertex() { + vec2 cell_coords = (u_terrain_inverse_transform * MODEL_MATRIX * vec4(VERTEX, 1)).xz; + + vec2 ps = vec2(1.0) / vec2(textureSize(u_terrain_heightmap, 0)); + vec2 uv = ps * cell_coords; + + // Get terrain normal + float k = 1.0; + float left = get_height(u_terrain_heightmap, uv + vec2(-ps.x, 0)) * k; + float right = get_height(u_terrain_heightmap, uv + vec2(ps.x, 0)) * k; + float back = get_height(u_terrain_heightmap, uv + vec2(0, -ps.y)) * k; + float fore = get_height(u_terrain_heightmap, uv + vec2(0, ps.y)) * k; + vec3 n = normalize(vec3(left - right, 2.0, back - fore)); + + n = u_terrain_normal_basis * n; + + float h = get_height(u_terrain_heightmap, uv); + VERTEX.y = h; + VERTEX += 1.0 * n; + NORMAL = n;//vec3(0.0, 1.0, 0.0); +} + +void fragment() { + float len = length(2.0 * UV - 1.0); + float g = clamp(1.0 - 15.0 * abs(0.9 - len), 0.0, 1.0); + ALBEDO = vec3(1.0, 0.1, 0.1); + ALPHA = g; +} diff --git a/game/addons/zylann.hterrain/tools/brush/no_blend.gdshader b/game/addons/zylann.hterrain/tools/brush/no_blend.gdshader new file mode 100644 index 0000000..8ae0f84 --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/no_blend.gdshader @@ -0,0 +1,6 @@ +shader_type canvas_item; +render_mode blend_disabled; + +void fragment() { + COLOR = texture(TEXTURE, UV); +} diff --git a/game/addons/zylann.hterrain/tools/brush/no_blend_rf.gdshader b/game/addons/zylann.hterrain/tools/brush/no_blend_rf.gdshader new file mode 100644 index 0000000..d0460de --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/no_blend_rf.gdshader @@ -0,0 +1,9 @@ +shader_type canvas_item; +render_mode blend_disabled; + +#include "res://addons/zylann.hterrain/shaders/include/heightmap.gdshaderinc" + +void fragment() { + float h = sample_heightmap(TEXTURE, UV); + COLOR = encode_height_to_viewport(h); +} diff --git a/game/addons/zylann.hterrain/tools/brush/painter.gd b/game/addons/zylann.hterrain/tools/brush/painter.gd new file mode 100644 index 0000000..fa4be98 --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/painter.gd @@ -0,0 +1,399 @@ + +# Core logic to paint a texture using shaders, with undo/redo support. +# Operations are delayed so results are only available the next frame. +# This doesn't implement UI or brush behavior, only rendering logic. +# +# Note: due to the absence of channel separation function in Image, +# you may need to use multiple painters at once if your application exploits multiple channels. +# Example: when painting a heightmap, it would be doable to output height in R, normalmap in GB, and +# then separate channels in two images at the end. + +@tool +extends Node + +const HT_Logger = preload("../../util/logger.gd") +const HT_Util = preload("../../util/util.gd") +const HT_NoBlendShader = preload("./no_blend.gdshader") +const HT_NoBlendRFShader = preload("./no_blend_rf.gdshader") + +const UNDO_CHUNK_SIZE = 64 + +# All painting shaders can use these common parameters +const SHADER_PARAM_SRC_TEXTURE = "u_src_texture" +const SHADER_PARAM_SRC_RECT = "u_src_rect" +const SHADER_PARAM_OPACITY = "u_opacity" + +const _API_SHADER_PARAMS = [ + SHADER_PARAM_SRC_TEXTURE, + SHADER_PARAM_SRC_RECT, + SHADER_PARAM_OPACITY +] + +# Emitted when a region of the painted texture actually changed. +# Note 1: the image might not have changed yet at this point. +# Note 2: the user could still be in the middle of dragging the brush. +signal texture_region_changed(rect) + +# Godot doesn't support 32-bit float rendering, so painting is limited to 16-bit depth. +# We should get this in Godot 4.0, either as Compute or renderer improvement +const _hdr_formats = [ + Image.FORMAT_RH, + Image.FORMAT_RGH, + Image.FORMAT_RGBH, + Image.FORMAT_RGBAH +] + +const _supported_formats = [ + Image.FORMAT_R8, + Image.FORMAT_RG8, + Image.FORMAT_RGB8, + Image.FORMAT_RGBA8 + # No longer supported since Godot 4 removed support for it in 2D viewports... +# Image.FORMAT_RH, +# Image.FORMAT_RGH, +# Image.FORMAT_RGBH, +# Image.FORMAT_RGBAH +] + +# - SubViewport (size of edited region + margin to allow quad rotation) +# |- Background +# | Fills pixels with unmodified source image. +# |- Brush sprite +# Size of actual brush, scaled/rotated, modifies source image. +# Assigned texture is the brush texture, src image is a shader param + +var _viewport : SubViewport +var _viewport_bg_sprite : Sprite2D +var _viewport_brush_sprite : Sprite2D +var _brush_size := 32 +var _brush_scale := 1.0 +var _brush_position := Vector2() +var _brush_opacity := 1.0 +var _brush_texture : Texture +var _last_brush_position := Vector2() +var _brush_material := ShaderMaterial.new() +var _no_blend_material : ShaderMaterial +var _image : Image +var _texture : ImageTexture +var _cmd_paint := false +var _pending_paint_render := false +var _modified_chunks := {} +var _modified_shader_params := {} + +var _debug_display : TextureRect +var _logger = HT_Logger.get_for(self) + + +func _init(): + _viewport = SubViewport.new() + _viewport.size = Vector2(_brush_size, _brush_size) + _viewport.render_target_update_mode = SubViewport.UPDATE_ONCE + _viewport.render_target_clear_mode = SubViewport.CLEAR_MODE_ONCE + #_viewport.hdr = false + # Require 4 components (RGBA) + _viewport.transparent_bg = true + # Apparently HDR doesn't work if this is set to 2D... so let's waste a depth buffer :/ + #_viewport.usage = Viewport.USAGE_2D + #_viewport.keep_3d_linear + + # There is no "blend_disabled" option on standard CanvasItemMaterial... + _no_blend_material = ShaderMaterial.new() + _no_blend_material.shader = HT_NoBlendShader + _viewport_bg_sprite = Sprite2D.new() + _viewport_bg_sprite.centered = false + _viewport_bg_sprite.material = _no_blend_material + _viewport.add_child(_viewport_bg_sprite) + + _viewport_brush_sprite = Sprite2D.new() + _viewport_brush_sprite.centered = true + _viewport_brush_sprite.material = _brush_material + _viewport_brush_sprite.position = _viewport.size / 2.0 + _viewport.add_child(_viewport_brush_sprite) + + add_child(_viewport) + + +func set_debug_display(dd: TextureRect): + _debug_display = dd + _debug_display.texture = _viewport.get_texture() + + +func set_image(image: Image, texture: ImageTexture): + assert((image == null and texture == null) or (image != null and texture != null)) + _image = image + _texture = texture + _viewport_bg_sprite.texture = _texture + _brush_material.set_shader_parameter(SHADER_PARAM_SRC_TEXTURE, _texture) + if image != null: + if image.get_format() == Image.FORMAT_RF: + # In case of RF all shaders must encode their fragment outputs in RGBA8, + # including the unmodified background, as Godot 4.0 does not support RF viewports + _no_blend_material.shader = HT_NoBlendRFShader + else: + _no_blend_material.shader = HT_NoBlendShader + # TODO HDR is required in order to paint heightmaps. + # Seems Godot 4.0 does not support it, so we have to wait for Godot 4.1... + #_viewport.hdr = image.get_format() in _hdr_formats + if (image.get_format() in _hdr_formats) and image.get_format() != Image.FORMAT_RF: + push_error("Godot 4.0 does not support HDR viewports for GPU-editing heightmaps! " + + "Only RF is supported using a bit packing hack.") + #print("PAINTER VIEWPORT HDR: ", _viewport.hdr) + + +# Sets the size of the brush in pixels. +# This will cause the internal viewport to resize, which is expensive. +# If you need to frequently change brush size during a paint stroke, prefer using scale instead. +func set_brush_size(new_size: int): + _brush_size = new_size + + +func get_brush_size() -> int: + return _brush_size + + +func set_brush_rotation(rotation: float): + _viewport_brush_sprite.rotation = rotation + + +func get_brush_rotation() -> float: + return _viewport_bg_sprite.rotation + + +# The difference between size and scale, is that size is in pixels, while scale is a multiplier. +# Scale is also a lot cheaper to change, so you may prefer changing it instead of size if that +# happens often during a painting stroke. +func set_brush_scale(s: float): + _brush_scale = clampf(s, 0.0, 1.0) + #_viewport_brush_sprite.scale = Vector2(s, s) + + +func get_brush_scale() -> float: + return _viewport_bg_sprite.scale.x + + +func set_brush_opacity(opacity: float): + _brush_opacity = clampf(opacity, 0.0, 1.0) + + +func get_brush_opacity() -> float: + return _brush_opacity + + +func set_brush_texture(texture: Texture): + _viewport_brush_sprite.texture = texture + + +func set_brush_shader(shader: Shader): + if _brush_material.shader != shader: + _brush_material.shader = shader + + +func set_brush_shader_param(p: String, v): + assert(not _API_SHADER_PARAMS.has(p)) + _modified_shader_params[p] = true + _brush_material.set_shader_parameter(p, v) + + +func clear_brush_shader_params(): + for key in _modified_shader_params: + _brush_material.set_shader_parameter(key, null) + _modified_shader_params.clear() + + +# If we want to be able to rotate the brush quad every frame, +# we must prepare a bigger viewport otherwise the quad will not fit inside +static func _get_size_fit_for_rotation(src_size: Vector2) -> Vector2i: + var d = int(ceilf(src_size.length())) + return Vector2i(d, d) + + +# You must call this from an `_input` function or similar. +func paint_input(center_pos: Vector2): + var vp_size := _get_size_fit_for_rotation(Vector2(_brush_size, _brush_size)) + if _viewport.size != vp_size: + # Do this lazily so the brush slider won't lag while adjusting it + # TODO An "sliding_ended" handling might produce better user experience + _viewport.size = vp_size + _viewport_brush_sprite.position = _viewport.size / 2.0 + + # Need to floor the position in case the brush has an odd size + var brush_pos := (center_pos - _viewport.size * 0.5).round() + _viewport.render_target_update_mode = SubViewport.UPDATE_ONCE + _viewport.render_target_clear_mode = SubViewport.CLEAR_MODE_ONCE + _viewport_bg_sprite.position = -brush_pos + _brush_position = brush_pos + _cmd_paint = true + + # We want this quad to have a specific size, regardless of the texture assigned to it + _viewport_brush_sprite.scale = \ + _brush_scale * Vector2(_brush_size, _brush_size) \ + / Vector2(_viewport_brush_sprite.texture.get_size()) + + # Using a Color because Godot doesn't understand vec4 + var rect := Color() + rect.r = brush_pos.x / _texture.get_width() + rect.g = brush_pos.y / _texture.get_height() + rect.b = float(_viewport.size.x) / float(_texture.get_width()) + rect.a = float(_viewport.size.y) / float(_texture.get_height()) + # In order to make sure that u_brush_rect is never bigger than the brush: + # 1. we ceil() the result of lower-left corner + # 2. we floor() the result of upper-right corner + # and then rederive width and height from the result +# var half_brush:Vector2 = Vector2(_brush_size, _brush_size) / 2 +# var brush_LL := (center_pos - half_brush).ceil() +# var brush_UR := (center_pos + half_brush).floor() +# rect.r = brush_LL.x / _texture.get_width() +# rect.g = brush_LL.y / _texture.get_height() +# rect.b = (brush_UR.x - brush_LL.x) / _texture.get_width() +# rect.a = (brush_UR.y - brush_LL.y) / _texture.get_height() + _brush_material.set_shader_parameter(SHADER_PARAM_SRC_RECT, rect) + _brush_material.set_shader_parameter(SHADER_PARAM_OPACITY, _brush_opacity) + + +# Don't commit until this is false +func is_operation_pending() -> bool: + return _pending_paint_render or _cmd_paint + + +# Applies changes to the Image, and returns modified chunks for UndoRedo. +func commit() -> Dictionary: + if is_operation_pending(): + _logger.error("Painter commit() was called while an operation is still pending") + return _commit_modified_chunks() + + +func has_modified_chunks() -> bool: + return len(_modified_chunks) > 0 + + +func _process(delta: float): + if _pending_paint_render: + _pending_paint_render = false + + #print("Paint result at frame ", Engine.get_frames_drawn()) + var viewport_image := _viewport.get_texture().get_image() + + if _image.get_format() == Image.FORMAT_RF: + # Reinterpret RGBA8 as RF. This assumes painting shaders encode the output properly. + assert(viewport_image.get_format() == Image.FORMAT_RGBA8) + viewport_image = Image.create_from_data( + viewport_image.get_width(), viewport_image.get_height(), false, Image.FORMAT_RF, + viewport_image.get_data()) + else: + viewport_image.convert(_image.get_format()) + + var brush_pos := _last_brush_position + + var dst_x : int = clamp(brush_pos.x, 0, _texture.get_width()) + var dst_y : int = clamp(brush_pos.y, 0, _texture.get_height()) + + var src_x : int = maxf(-brush_pos.x, 0) + var src_y : int = maxf(-brush_pos.y, 0) + var src_w : int = minf(maxf(_viewport.size.x - src_x, 0), _texture.get_width() - dst_x) + var src_h : int = minf(maxf(_viewport.size.y - src_y, 0), _texture.get_height() - dst_y) + + if src_w != 0 and src_h != 0: + _mark_modified_chunks(dst_x, dst_y, src_w, src_h) + HT_Util.update_texture_partial(_texture, viewport_image, + Rect2i(src_x, src_y, src_w, src_h), Vector2i(dst_x, dst_y)) + texture_region_changed.emit(Rect2(dst_x, dst_y, src_w, src_h)) + + # Input is handled just before process, so we still have to wait till next frame + if _cmd_paint: + _pending_paint_render = true + _last_brush_position = _brush_position + # Consume input + _cmd_paint = false + + +func _mark_modified_chunks(bx: int, by: int, bw: int, bh: int): + var cs := UNDO_CHUNK_SIZE + + var cmin_x := bx / cs + var cmin_y := by / cs + var cmax_x := (bx + bw - 1) / cs + 1 + var cmax_y := (by + bh - 1) / cs + 1 + + for cy in range(cmin_y, cmax_y): + for cx in range(cmin_x, cmax_x): + #print("Marking chunk ", Vector2(cx, cy)) + _modified_chunks[Vector2(cx, cy)] = true + + +func _commit_modified_chunks() -> Dictionary: + var time_before := Time.get_ticks_msec() + + var cs := UNDO_CHUNK_SIZE + var chunks_positions := [] + var chunks_initial_data := [] + var chunks_final_data := [] + + #_logger.debug("About to commit ", len(_modified_chunks), " chunks") + + # TODO get_data_partial() would be nice... + var final_image := _texture.get_image() + for cpos in _modified_chunks: + var cx : int = cpos.x + var cy : int = cpos.y + + var x := cx * cs + var y := cy * cs + var w : int = mini(cs, _image.get_width() - x) + var h : int = mini(cs, _image.get_height() - y) + + var rect := Rect2i(x, y, w, h) + var initial_data := _image.get_region(rect) + var final_data := final_image.get_region(rect) + + chunks_positions.append(cpos) + chunks_initial_data.append(initial_data) + chunks_final_data.append(final_data) + #_image_equals(initial_data, final_data) + + # TODO We could also just replace the image with `final_image`... + # TODO Use `final_data` instead? + _image.blit_rect(final_image, rect, rect.position) + + _modified_chunks.clear() + + var time_spent := Time.get_ticks_msec() - time_before + _logger.debug("Spent {0} ms to commit paint operation".format([time_spent])) + + return { + "chunk_positions": chunks_positions, + "chunk_initial_datas": chunks_initial_data, + "chunk_final_datas": chunks_final_data + } + + +# DEBUG +#func _input(event): +# if event is InputEventKey: +# if event.pressed: +# if event.control and event.scancode == KEY_SPACE: +# print("Saving painter viewport ", name) +# var im = _viewport.get_texture().get_data() +# im.convert(Image.FORMAT_RGBA8) +# im.save_png(str("test_painter_viewport_", name, ".png")) + + +#static func _image_equals(im_a: Image, im_b: Image) -> bool: +# if im_a.get_size() != im_b.get_size(): +# print("Diff size: ", im_a.get_size, ", ", im_b.get_size()) +# return false +# if im_a.get_format() != im_b.get_format(): +# print("Diff format: ", im_a.get_format(), ", ", im_b.get_format()) +# return false +# im_a.lock() +# im_b.lock() +# for y in im_a.get_height(): +# for x in im_a.get_width(): +# var ca = im_a.get_pixel(x, y) +# var cb = im_b.get_pixel(x, y) +# if ca != cb: +# print("Diff pixel ", x, ", ", y) +# return false +# im_a.unlock() +# im_b.unlock() +# print("SAME") +# return true diff --git a/game/addons/zylann.hterrain/tools/brush/settings_dialog/brush_settings_dialog.gd b/game/addons/zylann.hterrain/tools/brush/settings_dialog/brush_settings_dialog.gd new file mode 100644 index 0000000..71f81d1 --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/settings_dialog/brush_settings_dialog.gd @@ -0,0 +1,280 @@ +@tool +extends AcceptDialog + +const HT_Util = preload("../../../util/util.gd") +const HT_Brush = preload("../brush.gd") +const HT_Logger = preload("../../../util/logger.gd") +const HT_EditorUtil = preload("../../util/editor_util.gd") +const HT_SpinSlider = preload("../../util/spin_slider.gd") +const HT_Scratchpad = preload("./preview_scratchpad.gd") + +@onready var _scratchpad : HT_Scratchpad = $VB/HB/VB3/PreviewScratchpad + +@onready var _shape_list : ItemList = $VB/HB/VB/ShapeList +@onready var _remove_shape_button : Button = $VB/HB/VB/HBoxContainer/RemoveShape +@onready var _change_shape_button : Button = $VB/HB/VB/ChangeShape + +@onready var _size_slider : HT_SpinSlider = $VB/HB/VB2/Settings/Size +@onready var _opacity_slider : HT_SpinSlider = $VB/HB/VB2/Settings/Opacity +@onready var _pressure_enabled_checkbox : CheckBox = $VB/HB/VB2/Settings/PressureEnabled +@onready var _pressure_over_size_slider : HT_SpinSlider = $VB/HB/VB2/Settings/PressureOverSize +@onready var _pressure_over_opacity_slider : HT_SpinSlider = $VB/HB/VB2/Settings/PressureOverOpacity +@onready var _frequency_distance_slider : HT_SpinSlider = $VB/HB/VB2/Settings/FrequencyDistance +@onready var _frequency_time_slider : HT_SpinSlider = $VB/HB/VB2/Settings/FrequencyTime +@onready var _random_rotation_checkbox : CheckBox = $VB/HB/VB2/Settings/RandomRotation +@onready var _shape_cycling_checkbox : CheckBox = $VB/HB/VB2/Settings/ShapeCycling + +var _brush : HT_Brush +# This is a `EditorFileDialog`, +# but cannot type it because I want to be able to test it by running the scene. +# And when I run it, Godot does not allow to use `EditorFileDialog`. +var _load_image_dialog +# -1 means add, otherwise replace +var _load_image_index := -1 +var _logger = HT_Logger.get_for(self) + + +func _ready(): + if HT_Util.is_in_edited_scene(self): + return + + _size_slider.set_max_value(HT_Brush.MAX_SIZE_FOR_SLIDERS) + _size_slider.set_greater_max_value(HT_Brush.MAX_SIZE) + + # TESTING + if not Engine.is_editor_hint(): + setup_dialogs(self) + call_deferred("popup") + + +func set_brush(brush : HT_Brush): + assert(brush != null) + _brush = brush + _update_controls_from_brush() + + +# `base_control` can no longer be hinted as a `Control` because in Godot 4 it could be a +# window or dialog, which are no longer controls... +func setup_dialogs(base_control: Node): + assert(_load_image_dialog == null) + _load_image_dialog = HT_EditorUtil.create_open_file_dialog() + _load_image_dialog.file_mode = EditorFileDialog.FILE_MODE_OPEN_FILE + _load_image_dialog.add_filter("*.exr ; EXR files") + _load_image_dialog.unresizable = false + _load_image_dialog.access = EditorFileDialog.ACCESS_FILESYSTEM + _load_image_dialog.current_dir = HT_Brush.SHAPES_DIR + _load_image_dialog.file_selected.connect(_on_LoadImageDialog_file_selected) + _load_image_dialog.files_selected.connect(_on_LoadImageDialog_files_selected) + #base_control.add_child(_load_image_dialog) + # When a dialog opens another dialog, we get this error: + # "Transient parent has another exclusive child." + # Which is worked around by making the other dialog a child of the first one (I don't know why) + add_child(_load_image_dialog) + + +func _exit_tree(): + if _load_image_dialog != null: + _load_image_dialog.queue_free() + _load_image_dialog = null + + +func _get_shapes_from_gui() -> Array[Texture2D]: + var shapes : Array[Texture2D] = [] + for i in _shape_list.get_item_count(): + var icon : Texture2D = _shape_list.get_item_icon(i) + assert(icon != null) + shapes.append(icon) + return shapes + + +func _update_shapes_gui(shapes: Array[Texture2D]): + _shape_list.clear() + for shape in shapes: + assert(shape != null) + assert(shape is Texture2D) + _shape_list.add_icon_item(shape) + _update_shape_list_buttons() + + +func _on_AddShape_pressed(): + _load_image_index = -1 + _load_image_dialog.file_mode = EditorFileDialog.FILE_MODE_OPEN_FILES + _load_image_dialog.popup_centered_ratio(0.7) + + +func _on_RemoveShape_pressed(): + var selected_indices := _shape_list.get_selected_items() + if len(selected_indices) == 0: + return + + var index : int = selected_indices[0] + _shape_list.remove_item(index) + + var shapes := _get_shapes_from_gui() + for brush in _get_brushes(): + brush.set_shapes(shapes) + + _update_shape_list_buttons() + + +func _on_ShapeList_item_activated(index: int): + _request_modify_shape(index) + + +func _on_ChangeShape_pressed(): + var selected = _shape_list.get_selected_items() + if len(selected) == 0: + return + _request_modify_shape(selected[0]) + + +func _request_modify_shape(index: int): + _load_image_index = index + _load_image_dialog.file_mode = EditorFileDialog.FILE_MODE_OPEN_FILE + _load_image_dialog.popup_centered_ratio(0.7) + + +func _on_LoadImageDialog_files_selected(fpaths: PackedStringArray): + var shapes := _get_shapes_from_gui() + + for fpath in fpaths: + var tex := HT_Brush.load_shape_from_image_file(fpath, _logger) + if tex == null: + # Failed + continue + shapes.append(tex) + + for brush in _get_brushes(): + brush.set_shapes(shapes) + + _update_shapes_gui(shapes) + + +func _on_LoadImageDialog_file_selected(fpath: String): + var tex := HT_Brush.load_shape_from_image_file(fpath, _logger) + if tex == null: + # Failed + return + + var shapes := _get_shapes_from_gui() + if _load_image_index == -1 or _load_image_index >= len(shapes): + # Add + shapes.append(tex) + else: + # Replace + assert(_load_image_index >= 0) + shapes[_load_image_index] = tex + + for brush in _get_brushes(): + brush.set_shapes(shapes) + + _update_shapes_gui(shapes) + + +func _notification(what: int): + if what == NOTIFICATION_VISIBILITY_CHANGED: + # Testing the scratchpad because visibility can not only change before entering the tree + # since Godot 4 port, it can also change between entering the tree and being _ready... + if visible and _scratchpad != null: + _update_controls_from_brush() + + +func _update_controls_from_brush(): + var brush := _brush + + if brush == null: + # To allow testing + brush = _scratchpad.get_painter().get_brush() + + _update_shapes_gui(brush.get_shapes()) + + _size_slider.set_value(brush.get_size(), false) + _opacity_slider.set_value(brush.get_opacity() * 100.0, false) + _pressure_enabled_checkbox.button_pressed = brush.is_pressure_enabled() + _pressure_over_size_slider.set_value(brush.get_pressure_over_scale() * 100.0, false) + _pressure_over_opacity_slider.set_value(brush.get_pressure_over_opacity() * 100.0, false) + _frequency_distance_slider.set_value(brush.get_frequency_distance(), false) + _frequency_time_slider.set_value( + 1000.0 / maxf(0.1, float(brush.get_frequency_time_ms())), false) + _random_rotation_checkbox.button_pressed = brush.is_random_rotation_enabled() + _shape_cycling_checkbox.button_pressed = brush.is_shape_cycling_enabled() + + +func _on_ClearScratchpad_pressed(): + _scratchpad.reset_image() + + +func _on_Size_value_changed(value: float): + for brush in _get_brushes(): + brush.set_size(value) + + +func _on_Opacity_value_changed(value): + for brush in _get_brushes(): + brush.set_opacity(value / 100.0) + + +func _on_PressureEnabled_toggled(button_pressed): + for brush in _get_brushes(): + brush.set_pressure_enabled(button_pressed) + + +func _on_PressureOverSize_value_changed(value): + for brush in _get_brushes(): + brush.set_pressure_over_scale(value / 100.0) + + +func _on_PressureOverOpacity_value_changed(value): + for brush in _get_brushes(): + brush.set_pressure_over_opacity(value / 100.0) + + +func _on_FrequencyDistance_value_changed(value): + for brush in _get_brushes(): + brush.set_frequency_distance(value) + + +func _on_FrequencyTime_value_changed(fps): + fps = max(1.0, fps) + var ms = 1000.0 / fps + if is_equal_approx(fps, 60.0): + ms = 0 + for brush in _get_brushes(): + brush.set_frequency_time_ms(ms) + + +func _on_RandomRotation_toggled(button_pressed: bool): + for brush in _get_brushes(): + brush.set_random_rotation_enabled(button_pressed) + + +func _on_shape_cycling_toggled(button_pressed: bool): + for brush in _get_brushes(): + brush.set_shape_cycling_enabled(button_pressed) + + +func _get_brushes() -> Array[HT_Brush]: + if _brush != null: + # We edit both the preview brush and the terrain brush + # TODO Could we simply share the brush? + return [_brush, _scratchpad.get_painter().get_brush()] + # When testing the dialog in isolation, the edited brush might be null + return [_scratchpad.get_painter().get_brush()] + + +func _on_ShapeList_item_selected(index): + _update_shape_list_buttons() + for brush in _get_brushes(): + brush.set_shape_index(index) + + +func _update_shape_list_buttons(): + var selected_count := len(_shape_list.get_selected_items()) + # There must be at least one shape + _remove_shape_button.disabled = _shape_list.get_item_count() == 1 or selected_count == 0 + _change_shape_button.disabled = selected_count == 0 + + +func _on_shape_list_empty_clicked(at_position, mouse_button_index): + _update_shape_list_buttons() + diff --git a/game/addons/zylann.hterrain/tools/brush/settings_dialog/brush_settings_dialog.tscn b/game/addons/zylann.hterrain/tools/brush/settings_dialog/brush_settings_dialog.tscn new file mode 100644 index 0000000..7d7e07d --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/settings_dialog/brush_settings_dialog.tscn @@ -0,0 +1,211 @@ +[gd_scene load_steps=4 format=3 uid="uid://d2rt3wj8xkhp2"] + +[ext_resource type="PackedScene" path="res://addons/zylann.hterrain/tools/util/spin_slider.tscn" id="2"] +[ext_resource type="Script" path="res://addons/zylann.hterrain/tools/brush/settings_dialog/brush_settings_dialog.gd" id="3"] +[ext_resource type="PackedScene" uid="uid://ng00jipfeucy" path="res://addons/zylann.hterrain/tools/brush/settings_dialog/preview_scratchpad.tscn" id="4"] + +[node name="BrushSettingsDialog" type="AcceptDialog"] +title = "Brush settings" +size = Vector2i(700, 422) +min_size = Vector2i(700, 400) +script = ExtResource("3") + +[node name="VB" type="VBoxContainer" parent="."] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 8.0 +offset_top = 8.0 +offset_right = -8.0 +offset_bottom = -49.0 + +[node name="HB" type="HBoxContainer" parent="VB"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="VB" type="VBoxContainer" parent="VB/HB"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="Label" type="Label" parent="VB/HB/VB"] +layout_mode = 2 +text = "Shapes" + +[node name="ShapeList" type="ItemList" parent="VB/HB/VB"] +layout_mode = 2 +size_flags_vertical = 3 +fixed_icon_size = Vector2i(100, 100) + +[node name="ChangeShape" type="Button" parent="VB/HB/VB"] +layout_mode = 2 +disabled = true +text = "Change..." + +[node name="HBoxContainer" type="HBoxContainer" parent="VB/HB/VB"] +layout_mode = 2 + +[node name="AddShape" type="Button" parent="VB/HB/VB/HBoxContainer"] +layout_mode = 2 +text = "Add..." + +[node name="RemoveShape" type="Button" parent="VB/HB/VB/HBoxContainer"] +layout_mode = 2 +disabled = true +text = "Remove" + +[node name="VB2" type="VBoxContainer" parent="VB/HB"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="Label" type="Label" parent="VB/HB/VB2"] +layout_mode = 2 + +[node name="Settings" type="VBoxContainer" parent="VB/HB/VB2"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="Size" parent="VB/HB/VB2/Settings" instance=ExtResource("2")] +custom_minimum_size = Vector2(32, 28) +layout_mode = 2 +size_flags_horizontal = 3 +value = 32.0 +min_value = 2.0 +max_value = 500.0 +prefix = "Size:" +suffix = "px" +rounded = true +centered = true +allow_greater = true +greater_max_value = 4000.0 + +[node name="Opacity" parent="VB/HB/VB2/Settings" instance=ExtResource("2")] +custom_minimum_size = Vector2(32, 28) +layout_mode = 2 +size_flags_horizontal = 3 +value = 100.0 +max_value = 100.0 +prefix = "Opacity" +suffix = "%" +rounded = true +centered = true +greater_max_value = 10000.0 + +[node name="PressureEnabled" type="CheckBox" parent="VB/HB/VB2/Settings"] +layout_mode = 2 +text = "Enable pressure (pen tablets)" + +[node name="PressureOverSize" parent="VB/HB/VB2/Settings" instance=ExtResource("2")] +custom_minimum_size = Vector2(32, 28) +layout_mode = 2 +value = 50.0 +max_value = 100.0 +prefix = "Pressure affects size:" +suffix = "%" +centered = true +greater_max_value = 10000.0 + +[node name="PressureOverOpacity" parent="VB/HB/VB2/Settings" instance=ExtResource("2")] +custom_minimum_size = Vector2(32, 28) +layout_mode = 2 +value = 50.0 +max_value = 100.0 +prefix = "Pressure affects opacity:" +suffix = "%" +centered = true +greater_max_value = 10000.0 + +[node name="FrequencyTime" parent="VB/HB/VB2/Settings" instance=ExtResource("2")] +custom_minimum_size = Vector2(32, 28) +layout_mode = 2 +value = 60.0 +min_value = 1.0 +max_value = 60.0 +prefix = "Frequency time:" +suffix = "fps" +centered = true +greater_max_value = 10000.0 + +[node name="FrequencyDistance" parent="VB/HB/VB2/Settings" instance=ExtResource("2")] +custom_minimum_size = Vector2(32, 28) +layout_mode = 2 +max_value = 100.0 +prefix = "Frequency distance:" +suffix = "px" +centered = true +greater_max_value = 4000.0 + +[node name="RandomRotation" type="CheckBox" parent="VB/HB/VB2/Settings"] +layout_mode = 2 +text = "Random rotation" + +[node name="ShapeCycling" type="CheckBox" parent="VB/HB/VB2/Settings"] +layout_mode = 2 +text = "Shape cycling" + +[node name="HSeparator" type="HSeparator" parent="VB/HB/VB2/Settings"] +visible = false +layout_mode = 2 + +[node name="SizeLimitHB" type="HBoxContainer" parent="VB/HB/VB2/Settings"] +visible = false +layout_mode = 2 + +[node name="Label" type="Label" parent="VB/HB/VB2/Settings/SizeLimitHB"] +layout_mode = 2 +mouse_filter = 0 +text = "Size limit:" + +[node name="SizeLimit" type="SpinBox" parent="VB/HB/VB2/Settings/SizeLimitHB"] +layout_mode = 2 +size_flags_horizontal = 3 +min_value = 1.0 +max_value = 1000.0 +value = 200.0 + +[node name="HSeparator2" type="HSeparator" parent="VB/HB/VB2/Settings"] +visible = false +layout_mode = 2 + +[node name="HB" type="HBoxContainer" parent="VB/HB/VB2/Settings"] +visible = false +layout_mode = 2 + +[node name="Button" type="Button" parent="VB/HB/VB2/Settings/HB"] +layout_mode = 2 +text = "Load preset..." + +[node name="Button2" type="Button" parent="VB/HB/VB2/Settings/HB"] +layout_mode = 2 +text = "Save preset..." + +[node name="VB3" type="VBoxContainer" parent="VB/HB"] +layout_mode = 2 + +[node name="Label" type="Label" parent="VB/HB/VB3"] +layout_mode = 2 +text = "Scratchpad" + +[node name="PreviewScratchpad" parent="VB/HB/VB3" instance=ExtResource("4")] +custom_minimum_size = Vector2(200, 300) +layout_mode = 2 + +[node name="ClearScratchpad" type="Button" parent="VB/HB/VB3"] +layout_mode = 2 +text = "Clear" + +[connection signal="empty_clicked" from="VB/HB/VB/ShapeList" to="." method="_on_shape_list_empty_clicked"] +[connection signal="item_activated" from="VB/HB/VB/ShapeList" to="." method="_on_ShapeList_item_activated"] +[connection signal="item_selected" from="VB/HB/VB/ShapeList" to="." method="_on_ShapeList_item_selected"] +[connection signal="pressed" from="VB/HB/VB/ChangeShape" to="." method="_on_ChangeShape_pressed"] +[connection signal="pressed" from="VB/HB/VB/HBoxContainer/AddShape" to="." method="_on_AddShape_pressed"] +[connection signal="pressed" from="VB/HB/VB/HBoxContainer/RemoveShape" to="." method="_on_RemoveShape_pressed"] +[connection signal="value_changed" from="VB/HB/VB2/Settings/Size" to="." method="_on_Size_value_changed"] +[connection signal="value_changed" from="VB/HB/VB2/Settings/Opacity" to="." method="_on_Opacity_value_changed"] +[connection signal="toggled" from="VB/HB/VB2/Settings/PressureEnabled" to="." method="_on_PressureEnabled_toggled"] +[connection signal="value_changed" from="VB/HB/VB2/Settings/PressureOverSize" to="." method="_on_PressureOverSize_value_changed"] +[connection signal="value_changed" from="VB/HB/VB2/Settings/PressureOverOpacity" to="." method="_on_PressureOverOpacity_value_changed"] +[connection signal="value_changed" from="VB/HB/VB2/Settings/FrequencyTime" to="." method="_on_FrequencyTime_value_changed"] +[connection signal="value_changed" from="VB/HB/VB2/Settings/FrequencyDistance" to="." method="_on_FrequencyDistance_value_changed"] +[connection signal="toggled" from="VB/HB/VB2/Settings/RandomRotation" to="." method="_on_RandomRotation_toggled"] +[connection signal="toggled" from="VB/HB/VB2/Settings/ShapeCycling" to="." method="_on_shape_cycling_toggled"] +[connection signal="pressed" from="VB/HB/VB3/ClearScratchpad" to="." method="_on_ClearScratchpad_pressed"] diff --git a/game/addons/zylann.hterrain/tools/brush/settings_dialog/preview_painter.gd b/game/addons/zylann.hterrain/tools/brush/settings_dialog/preview_painter.gd new file mode 100644 index 0000000..be52072 --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/settings_dialog/preview_painter.gd @@ -0,0 +1,41 @@ +@tool +extends Node + +const HT_Painter = preload("./../painter.gd") +const HT_Brush = preload("./../brush.gd") + +const HT_ColorShader = preload("../shaders/color.gdshader") + +var _painter : HT_Painter +var _brush : HT_Brush + + +func _init(): + var p = HT_Painter.new() + # The name is just for debugging + p.set_name("Painter") + add_child(p) + _painter = p + + _brush = HT_Brush.new() + + +func set_image_texture(image: Image, texture: ImageTexture): + _painter.set_image(image, texture) + + +func get_brush() -> HT_Brush: + return _brush + + +# This may be called from an `_input` callback +func paint_input(position: Vector2, pressure: float): + var p : HT_Painter = _painter + + if not _brush.configure_paint_input([p], position, pressure): + return + + p.set_brush_shader(HT_ColorShader) + p.set_brush_shader_param("u_color", Color(0,0,0,1)) + #p.set_image(_image, _texture) + p.paint_input(position) diff --git a/game/addons/zylann.hterrain/tools/brush/settings_dialog/preview_scratchpad.gd b/game/addons/zylann.hterrain/tools/brush/settings_dialog/preview_scratchpad.gd new file mode 100644 index 0000000..cec0728 --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/settings_dialog/preview_scratchpad.gd @@ -0,0 +1,70 @@ +@tool +extends Control + +const HT_PreviewPainter = preload("./preview_painter.gd") +# TODO Can't preload because it causes the plugin to fail loading if assets aren't imported +#const HT_DefaultBrushTexture = preload("../shapes/round2.exr") +const HT_Brush = preload("../brush.gd") +const HT_Logger = preload("../../../util/logger.gd") +const HT_EditorUtil = preload("../../util/editor_util.gd") +const HT_Util = preload("../../../util/util.gd") + +@onready var _texture_rect : TextureRect = $TextureRect +@onready var _painter : HT_PreviewPainter = $Painter + +var _logger := HT_Logger.get_for(self) + + +func _ready(): + if HT_Util.is_in_edited_scene(self): + # If it runs in the edited scene, + # saving the scene would also save the ImageTexture in it... + return + reset_image() + # Default so it doesn't crash when painting and can be tested + var default_brush_texture = \ + HT_EditorUtil.load_texture(HT_Brush.DEFAULT_BRUSH_TEXTURE_PATH, _logger) + _painter.get_brush().set_shapes([default_brush_texture]) + + +func reset_image(): + var image = Image.create(_texture_rect.size.x, _texture_rect.size.y, false, Image.FORMAT_RGB8) + image.fill(Color(1,1,1)) + + # TEST +# var fnl = FastNoiseLite.new() +# for y in image.get_height(): +# for x in image.get_width(): +# var g = 0.5 + 0.5 * fnl.get_noise_2d(x, y) +# image.set_pixel(x, y, Color(g, g, g, 1.0)) + + var texture = ImageTexture.create_from_image(image) + _texture_rect.texture = texture + _painter.set_image_texture(image, texture) + + +func get_painter() -> HT_PreviewPainter: + return _painter + + +func _gui_input(event): + if event is InputEventMouseMotion: + if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT): + _painter.paint_input(event.position, event.pressure) + queue_redraw() + + elif event is InputEventMouseButton: + if event.button_index == MOUSE_BUTTON_LEFT: + if event.pressed: + # TODO `pressure` is not available on button events + # So I have to assume zero... which means clicks do not paint anything? + _painter.paint_input(event.position, 0.0) + else: + _painter.get_brush().on_paint_end() + + +func _draw(): + var mpos = get_local_mouse_position() + var brush = _painter.get_brush() + draw_arc(mpos, 0.5 * brush.get_size(), -PI, PI, 32, Color(1, 0.2, 0.2), 2.0, true) + diff --git a/game/addons/zylann.hterrain/tools/brush/settings_dialog/preview_scratchpad.tscn b/game/addons/zylann.hterrain/tools/brush/settings_dialog/preview_scratchpad.tscn new file mode 100644 index 0000000..0b50c91 --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/settings_dialog/preview_scratchpad.tscn @@ -0,0 +1,22 @@ +[gd_scene load_steps=3 format=3 uid="uid://ng00jipfeucy"] + +[ext_resource type="Script" path="res://addons/zylann.hterrain/tools/brush/settings_dialog/preview_scratchpad.gd" id="1"] +[ext_resource type="Script" path="res://addons/zylann.hterrain/tools/brush/settings_dialog/preview_painter.gd" id="2"] + +[node name="PreviewScratchpad" type="Control"] +clip_contents = true +layout_mode = 3 +anchors_preset = 0 +offset_right = 200.0 +offset_bottom = 274.0 +script = ExtResource("1") + +[node name="Painter" type="Node" parent="."] +script = ExtResource("2") + +[node name="TextureRect" type="TextureRect" parent="."] +show_behind_parent = true +layout_mode = 0 +anchor_right = 1.0 +anchor_bottom = 1.0 +stretch_mode = 5 diff --git a/game/addons/zylann.hterrain/tools/brush/shaders/alpha.gdshader b/game/addons/zylann.hterrain/tools/brush/shaders/alpha.gdshader new file mode 100644 index 0000000..f8a05c5 --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/shaders/alpha.gdshader @@ -0,0 +1,19 @@ +shader_type canvas_item; +render_mode blend_disabled; + +uniform sampler2D u_src_texture; +uniform vec4 u_src_rect; +uniform float u_opacity = 1.0; +uniform float u_value = 1.0; + +vec2 get_src_uv(vec2 screen_uv) { + vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw; + return uv; +} + +void fragment() { + float brush_value = u_opacity * texture(TEXTURE, UV).r; + + vec4 src = texture(u_src_texture, get_src_uv(SCREEN_UV)); + COLOR = vec4(src.rgb, mix(src.a, u_value, brush_value)); +} diff --git a/game/addons/zylann.hterrain/tools/brush/shaders/color.gdshader b/game/addons/zylann.hterrain/tools/brush/shaders/color.gdshader new file mode 100644 index 0000000..eec65f5 --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/shaders/color.gdshader @@ -0,0 +1,68 @@ +shader_type canvas_item; +render_mode blend_disabled; + +#include "res://addons/zylann.hterrain/shaders/include/heightmap.gdshaderinc" + +uniform sampler2D u_src_texture; +uniform vec4 u_src_rect; +uniform float u_opacity = 1.0; +uniform vec4 u_color = vec4(1.0); +uniform sampler2D u_heightmap; +uniform float u_normal_min_y = 0.0; +uniform float u_normal_max_y = 1.0; + +vec2 get_src_uv(vec2 screen_uv) { + vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw; + return uv; +} + +float get_height(sampler2D heightmap, vec2 uv) { + return sample_heightmap(heightmap, uv); +} + +vec3 get_normal(sampler2D heightmap, vec2 pos) { + vec2 ps = vec2(1.0) / vec2(textureSize(heightmap, 0)); + float hnx = get_height(heightmap, pos + vec2(-ps.x, 0.0)); + float hpx = get_height(heightmap, pos + vec2(ps.x, 0.0)); + float hny = get_height(heightmap, pos + vec2(0.0, -ps.y)); + float hpy = get_height(heightmap, pos + vec2(0.0, ps.y)); + return normalize(vec3(hnx - hpx, 2.0, hpy - hny)); +} + +// Limits painting based on the slope, with a bit of falloff +float apply_slope_limit(float brush_value, vec3 normal, float normal_min_y, float normal_max_y) { + float normal_falloff = 0.2; + + // If an edge is at min/max, make sure it won't be affected by falloff + normal_min_y = normal_min_y <= 0.0 ? -2.0 : normal_min_y; + normal_max_y = normal_max_y >= 1.0 ? 2.0 : normal_max_y; + + brush_value *= 1.0 - smoothstep( + normal_max_y - normal_falloff, + normal_max_y + normal_falloff, normal.y); + + brush_value *= smoothstep( + normal_min_y - normal_falloff, + normal_min_y + normal_falloff, normal.y); + + return brush_value; +} + +void fragment() { + float brush_value = u_opacity * texture(TEXTURE, UV).r; + + vec2 src_uv = get_src_uv(SCREEN_UV); + vec3 normal = get_normal(u_heightmap, src_uv); + brush_value = apply_slope_limit(brush_value, normal, u_normal_min_y, u_normal_max_y); + + vec4 src = texture(u_src_texture, src_uv); + + // Despite hints, albedo textures render darker. + // Trying to undo sRGB does not work because of 8-bit precision loss + // that would occur either in texture, or on the source image. + // So it's not possible to use viewports to paint albedo... + //src.rgb = pow(src.rgb, vec3(0.4545)); + + vec4 col = vec4(mix(src.rgb, u_color.rgb, brush_value), src.a); + COLOR = col; +} diff --git a/game/addons/zylann.hterrain/tools/brush/shaders/erode.gdshader b/game/addons/zylann.hterrain/tools/brush/shaders/erode.gdshader new file mode 100644 index 0000000..669f4d7 --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/shaders/erode.gdshader @@ -0,0 +1,64 @@ +shader_type canvas_item; +render_mode blend_disabled; + +#include "res://addons/zylann.hterrain/shaders/include/heightmap.gdshaderinc" + +uniform sampler2D u_src_texture; +uniform vec4 u_src_rect; +uniform float u_opacity = 1.0; +uniform vec4 u_color = vec4(1.0); + +vec2 get_src_uv(vec2 screen_uv) { + vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw; + return uv; +} + +// float get_noise(vec2 pos) { +// return fract(sin(dot(pos.xy ,vec2(12.9898,78.233))) * 43758.5453); +// } + +float get_height(sampler2D heightmap, vec2 uv) { + return sample_heightmap(heightmap, uv); +} + +float erode(sampler2D heightmap, vec2 uv, vec2 pixel_size, float weight) { + float r = 3.0; + + // Divide so the shader stays neighbor dependent 1 pixel across. + // For this to work, filtering must be enabled. + vec2 eps = pixel_size / (0.99 * r); + + float h = get_height(heightmap, uv); + float eh = h; + //float dh = h; + + // Morphology with circular structuring element + for (float y = -r; y <= r; ++y) { + for (float x = -r; x <= r; ++x) { + + vec2 p = vec2(x, y); + float nh = get_height(heightmap, uv + p * eps); + + float s = max(length(p) - r, 0); + eh = min(eh, nh + s); + + //s = min(r - length(p), 0); + //dh = max(dh, nh + s); + } + } + + eh = mix(h, eh, weight); + //dh = mix(h, dh, u_weight); + + float ph = eh;//mix(eh, dh, u_dilation); + + return ph; +} + +void fragment() { + float brush_value = u_opacity * texture(TEXTURE, UV).r; + vec2 src_pixel_size = 1.0 / vec2(textureSize(u_src_texture, 0)); + float ph = erode(u_src_texture, get_src_uv(SCREEN_UV), src_pixel_size, brush_value); + //ph += brush_value * 0.35; + COLOR = encode_height_to_viewport(ph); +} diff --git a/game/addons/zylann.hterrain/tools/brush/shaders/flatten.gdshader b/game/addons/zylann.hterrain/tools/brush/shaders/flatten.gdshader new file mode 100644 index 0000000..c51f03a --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/shaders/flatten.gdshader @@ -0,0 +1,22 @@ +shader_type canvas_item; +render_mode blend_disabled; + +#include "res://addons/zylann.hterrain/shaders/include/heightmap.gdshaderinc" + +uniform sampler2D u_src_texture; +uniform vec4 u_src_rect; +uniform float u_opacity = 1.0; +uniform float u_flatten_value; + +vec2 get_src_uv(vec2 screen_uv) { + vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw; + return uv; +} + +void fragment() { + float brush_value = u_opacity * texture(TEXTURE, UV).r; + + float src_h = sample_heightmap(u_src_texture, get_src_uv(SCREEN_UV)); + float h = mix(src_h, u_flatten_value, brush_value); + COLOR = encode_height_to_viewport(h); +} diff --git a/game/addons/zylann.hterrain/tools/brush/shaders/level.gdshader b/game/addons/zylann.hterrain/tools/brush/shaders/level.gdshader new file mode 100644 index 0000000..4721b43 --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/shaders/level.gdshader @@ -0,0 +1,45 @@ +shader_type canvas_item; +render_mode blend_disabled; + +#include "res://addons/zylann.hterrain/shaders/include/heightmap.gdshaderinc" + +uniform sampler2D u_src_texture; +uniform vec4 u_src_rect; +uniform float u_opacity = 1.0; +uniform float u_factor = 1.0; + +vec2 get_src_uv(vec2 screen_uv) { + vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw; + return uv; +} + +float get_height(sampler2D heightmap, vec2 uv) { + return sample_heightmap(heightmap, uv); +} + +// TODO Could actually level to whatever height the brush was at the beginning of the stroke? + +void fragment() { + float brush_value = u_factor * u_opacity * texture(TEXTURE, UV).r; + + // The heightmap does not have mipmaps, + // so we need to use an approximation of average. + // This is not a very good one though... + float dst_h = 0.0; + vec2 uv_min = vec2(u_src_rect.xy); + vec2 uv_max = vec2(u_src_rect.xy + u_src_rect.zw); + for (int i = 0; i < 5; ++i) { + for (int j = 0; j < 5; ++j) { + float x = mix(uv_min.x, uv_max.x, float(i) / 4.0); + float y = mix(uv_min.y, uv_max.y, float(j) / 4.0); + float h = get_height(u_src_texture, vec2(x, y)); + dst_h += h; + } + } + dst_h /= (5.0 * 5.0); + + // TODO I have no idea if this will check out + float src_h = get_height(u_src_texture, get_src_uv(SCREEN_UV)); + float h = mix(src_h, dst_h, brush_value); + COLOR = encode_height_to_viewport(h); +} diff --git a/game/addons/zylann.hterrain/tools/brush/shaders/raise.gdshader b/game/addons/zylann.hterrain/tools/brush/shaders/raise.gdshader new file mode 100644 index 0000000..10ee982 --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/shaders/raise.gdshader @@ -0,0 +1,22 @@ +shader_type canvas_item; +render_mode blend_disabled; + +#include "res://addons/zylann.hterrain/shaders/include/heightmap.gdshaderinc" + +uniform sampler2D u_src_texture; +uniform vec4 u_src_rect; +uniform float u_opacity = 1.0; +uniform float u_factor = 1.0; + +vec2 get_src_uv(vec2 screen_uv) { + vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw; + return uv; +} + +void fragment() { + float brush_value = u_factor * u_opacity * texture(TEXTURE, UV).r; + + float src_h = sample_heightmap(u_src_texture, get_src_uv(SCREEN_UV)); + float h = src_h + brush_value; + COLOR = encode_height_to_viewport(h); +} diff --git a/game/addons/zylann.hterrain/tools/brush/shaders/smooth.gdshader b/game/addons/zylann.hterrain/tools/brush/shaders/smooth.gdshader new file mode 100644 index 0000000..27123e4 --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/shaders/smooth.gdshader @@ -0,0 +1,34 @@ +shader_type canvas_item; +render_mode blend_disabled; + +#include "res://addons/zylann.hterrain/shaders/include/heightmap.gdshaderinc" + +uniform sampler2D u_src_texture; +uniform vec4 u_src_rect; +uniform float u_opacity = 1.0; +uniform float u_factor = 1.0; + +vec2 get_src_uv(vec2 screen_uv) { + vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw; + return uv; +} + +float get_height(sampler2D heightmap, vec2 uv) { + return sample_heightmap(heightmap, uv); +} + +void fragment() { + float brush_value = u_factor * u_opacity * texture(TEXTURE, UV).r; + + vec2 src_pixel_size = 1.0 / vec2(textureSize(u_src_texture, 0)); + vec2 src_uv = get_src_uv(SCREEN_UV); + vec2 offset = src_pixel_size; + float src_nx = get_height(u_src_texture, src_uv - vec2(offset.x, 0.0)); + float src_px = get_height(u_src_texture, src_uv + vec2(offset.x, 0.0)); + float src_ny = get_height(u_src_texture, src_uv - vec2(0.0, offset.y)); + float src_py = get_height(u_src_texture, src_uv + vec2(0.0, offset.y)); + float src_h = get_height(u_src_texture, src_uv); + float dst_h = (src_h + src_nx + src_px + src_ny + src_py) * 0.2; + float h = mix(src_h, dst_h, brush_value); + COLOR = encode_height_to_viewport(h); +} diff --git a/game/addons/zylann.hterrain/tools/brush/shaders/splat16.gdshader b/game/addons/zylann.hterrain/tools/brush/shaders/splat16.gdshader new file mode 100644 index 0000000..68ebaa8 --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/shaders/splat16.gdshader @@ -0,0 +1,81 @@ +shader_type canvas_item; +render_mode blend_disabled; + +#include "res://addons/zylann.hterrain/shaders/include/heightmap.gdshaderinc" + +uniform sampler2D u_src_texture; +uniform vec4 u_src_rect; +uniform float u_opacity = 1.0; +uniform vec4 u_splat = vec4(1.0, 0.0, 0.0, 0.0); +uniform sampler2D u_other_splatmap_1; +uniform sampler2D u_other_splatmap_2; +uniform sampler2D u_other_splatmap_3; +uniform sampler2D u_heightmap; +uniform float u_normal_min_y = 0.0; +uniform float u_normal_max_y = 1.0; + +vec2 get_src_uv(vec2 screen_uv) { + vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw; + return uv; +} + +float sum(vec4 v) { + return v.x + v.y + v.z + v.w; +} + +float get_height(sampler2D heightmap, vec2 uv) { + return sample_heightmap(heightmap, uv); +} + +vec3 get_normal(sampler2D heightmap, vec2 pos) { + vec2 ps = vec2(1.0) / vec2(textureSize(heightmap, 0)); + float hnx = get_height(heightmap, pos + vec2(-ps.x, 0.0)); + float hpx = get_height(heightmap, pos + vec2(ps.x, 0.0)); + float hny = get_height(heightmap, pos + vec2(0.0, -ps.y)); + float hpy = get_height(heightmap, pos + vec2(0.0, ps.y)); + return normalize(vec3(hnx - hpx, 2.0, hpy - hny)); +} + +// Limits painting based on the slope, with a bit of falloff +float apply_slope_limit(float brush_value, vec3 normal, float normal_min_y, float normal_max_y) { + float normal_falloff = 0.2; + + // If an edge is at min/max, make sure it won't be affected by falloff + normal_min_y = normal_min_y <= 0.0 ? -2.0 : normal_min_y; + normal_max_y = normal_max_y >= 1.0 ? 2.0 : normal_max_y; + + brush_value *= 1.0 - smoothstep( + normal_max_y - normal_falloff, + normal_max_y + normal_falloff, normal.y); + + brush_value *= smoothstep( + normal_min_y - normal_falloff, + normal_min_y + normal_falloff, normal.y); + + return brush_value; +} + +void fragment() { + float brush_value = u_opacity * texture(TEXTURE, UV).r; + + vec2 src_uv = get_src_uv(SCREEN_UV); + vec3 normal = get_normal(u_heightmap, src_uv); + brush_value = apply_slope_limit(brush_value, normal, u_normal_min_y, u_normal_max_y); + + // It is assumed 3 other renders are done the same with the other 3 + vec4 src0 = texture(u_src_texture, src_uv); + vec4 src1 = texture(u_other_splatmap_1, src_uv); + vec4 src2 = texture(u_other_splatmap_2, src_uv); + vec4 src3 = texture(u_other_splatmap_3, src_uv); + float t = brush_value; + vec4 s0 = mix(src0, u_splat, t); + vec4 s1 = mix(src1, vec4(0.0), t); + vec4 s2 = mix(src2, vec4(0.0), t); + vec4 s3 = mix(src3, vec4(0.0), t); + float sum = sum(s0) + sum(s1) + sum(s2) + sum(s3); + s0 /= sum; + s1 /= sum; + s2 /= sum; + s3 /= sum; + COLOR = s0; +} diff --git a/game/addons/zylann.hterrain/tools/brush/shaders/splat4.gdshader b/game/addons/zylann.hterrain/tools/brush/shaders/splat4.gdshader new file mode 100644 index 0000000..1291dbd --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/shaders/splat4.gdshader @@ -0,0 +1,63 @@ +shader_type canvas_item; +render_mode blend_disabled; + +#include "res://addons/zylann.hterrain/shaders/include/heightmap.gdshaderinc" + +uniform sampler2D u_src_texture; +uniform vec4 u_src_rect; +uniform float u_opacity = 1.0; +uniform vec4 u_splat = vec4(1.0, 0.0, 0.0, 0.0); +uniform sampler2D u_heightmap; +uniform float u_normal_min_y = 0.0; +uniform float u_normal_max_y = 1.0; +//uniform float u_normal_falloff = 0.0; + +vec2 get_src_uv(vec2 screen_uv) { + vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw; + return uv; +} + +float get_height(sampler2D heightmap, vec2 uv) { + return sample_heightmap(heightmap, uv); +} + +vec3 get_normal(sampler2D heightmap, vec2 pos) { + vec2 ps = vec2(1.0) / vec2(textureSize(heightmap, 0)); + float hnx = get_height(heightmap, pos + vec2(-ps.x, 0.0)); + float hpx = get_height(heightmap, pos + vec2(ps.x, 0.0)); + float hny = get_height(heightmap, pos + vec2(0.0, -ps.y)); + float hpy = get_height(heightmap, pos + vec2(0.0, ps.y)); + return normalize(vec3(hnx - hpx, 2.0, hpy - hny)); +} + +// Limits painting based on the slope, with a bit of falloff +float apply_slope_limit(float brush_value, vec3 normal, float normal_min_y, float normal_max_y) { + float normal_falloff = 0.2; + + // If an edge is at min/max, make sure it won't be affected by falloff + normal_min_y = normal_min_y <= 0.0 ? -2.0 : normal_min_y; + normal_max_y = normal_max_y >= 1.0 ? 2.0 : normal_max_y; + + brush_value *= 1.0 - smoothstep( + normal_max_y - normal_falloff, + normal_max_y + normal_falloff, normal.y); + + brush_value *= smoothstep( + normal_min_y - normal_falloff, + normal_min_y + normal_falloff, normal.y); + + return brush_value; +} + +void fragment() { + float brush_value = u_opacity * texture(TEXTURE, UV).r; + + vec2 src_uv = get_src_uv(SCREEN_UV); + vec3 normal = get_normal(u_heightmap, src_uv); + brush_value = apply_slope_limit(brush_value, normal, u_normal_min_y, u_normal_max_y); + + vec4 src_splat = texture(u_src_texture, src_uv); + vec4 s = mix(src_splat, u_splat, brush_value); + s = s / (s.r + s.g + s.b + s.a); + COLOR = s; +} diff --git a/game/addons/zylann.hterrain/tools/brush/shaders/splat_indexed.gdshader b/game/addons/zylann.hterrain/tools/brush/shaders/splat_indexed.gdshader new file mode 100644 index 0000000..9828068 --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/shaders/splat_indexed.gdshader @@ -0,0 +1,89 @@ +shader_type canvas_item; +render_mode blend_disabled; + +uniform sampler2D u_src_texture; +uniform vec4 u_src_rect; +uniform float u_opacity = 1.0; +uniform int u_texture_index; +uniform int u_mode; // 0: output index, 1: output weight +uniform sampler2D u_index_map; +uniform sampler2D u_weight_map; + +vec2 get_src_uv(vec2 screen_uv) { + vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw; + return uv; +} + +void fragment() { + float brush_value = u_opacity * texture(TEXTURE, UV).r; + + vec2 src_uv = get_src_uv(SCREEN_UV); + vec4 iv = texture(u_index_map, src_uv); + vec4 wv = texture(u_weight_map, src_uv); + + float i[3] = {iv.r, iv.g, iv.b}; + float w[3] = {wv.r, wv.g, wv.b}; + + if (brush_value > 0.0) { + float texture_index_f = float(u_texture_index) / 255.0; + int ci = u_texture_index % 3; + + float cm[3] = {-1.0, -1.0, -1.0}; + cm[ci] = 1.0; + + // Decompress third weight to make computations easier + w[2] = 1.0 - w[0] - w[1]; + + if (abs(i[ci] - texture_index_f) > 0.001) { + // Pixel does not have our texture index, + // transfer its weight to other components first + if (w[ci] > brush_value) { + w[0] -= cm[0] * brush_value; + w[1] -= cm[1] * brush_value; + w[2] -= cm[2] * brush_value; + + } else if (w[ci] >= 0.f) { + w[ci] = 0.f; + i[ci] = texture_index_f; + } + + } else { + // Pixel has our texture index, increase its weight + if (w[ci] + brush_value < 1.f) { + w[0] += cm[0] * brush_value; + w[1] += cm[1] * brush_value; + w[2] += cm[2] * brush_value; + + } else { + // Pixel weight is full, we can set all components to the same index. + // Need to nullify other weights because they would otherwise never reach + // zero due to normalization + w[0] = 0.0; + w[1] = 0.0; + w[2] = 0.0; + + w[ci] = 1.0; + + i[0] = texture_index_f; + i[1] = texture_index_f; + i[2] = texture_index_f; + } + } + + w[0] = clamp(w[0], 0.0, 1.0); + w[1] = clamp(w[1], 0.0, 1.0); + w[2] = clamp(w[2], 0.0, 1.0); + + // Renormalize + float sum = w[0] + w[1] + w[2]; + w[0] /= sum; + w[1] /= sum; + w[2] /= sum; + } + + if (u_mode == 0) { + COLOR = vec4(i[0], i[1], i[2], 1.0); + } else { + COLOR = vec4(w[0], w[1], w[2], 1.0); + } +} diff --git a/game/addons/zylann.hterrain/tools/brush/shapes/acrylic1.exr b/game/addons/zylann.hterrain/tools/brush/shapes/acrylic1.exr Binary files differnew file mode 100644 index 0000000..14b66d6 --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/shapes/acrylic1.exr diff --git a/game/addons/zylann.hterrain/tools/brush/shapes/acrylic1.exr.import b/game/addons/zylann.hterrain/tools/brush/shapes/acrylic1.exr.import new file mode 100644 index 0000000..c547891 --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/shapes/acrylic1.exr.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cua2gxgj2pum5" +path="res://.godot/imported/acrylic1.exr-8a4b622f104c607118d296791ee118f3.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/zylann.hterrain/tools/brush/shapes/acrylic1.exr" +dest_files=["res://.godot/imported/acrylic1.exr-8a4b622f104c607118d296791ee118f3.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/addons/zylann.hterrain/tools/brush/shapes/round0.exr b/game/addons/zylann.hterrain/tools/brush/shapes/round0.exr Binary files differnew file mode 100644 index 0000000..e91d97e --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/shapes/round0.exr diff --git a/game/addons/zylann.hterrain/tools/brush/shapes/round0.exr.import b/game/addons/zylann.hterrain/tools/brush/shapes/round0.exr.import new file mode 100644 index 0000000..6308ee6 --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/shapes/round0.exr.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dekau2j7fx14d" +path="res://.godot/imported/round0.exr-fc6d691e8892911b1b4496769ee75dbb.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/zylann.hterrain/tools/brush/shapes/round0.exr" +dest_files=["res://.godot/imported/round0.exr-fc6d691e8892911b1b4496769ee75dbb.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/addons/zylann.hterrain/tools/brush/shapes/round1.exr b/game/addons/zylann.hterrain/tools/brush/shapes/round1.exr Binary files differnew file mode 100644 index 0000000..f6931ba --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/shapes/round1.exr diff --git a/game/addons/zylann.hterrain/tools/brush/shapes/round1.exr.import b/game/addons/zylann.hterrain/tools/brush/shapes/round1.exr.import new file mode 100644 index 0000000..e64db1a --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/shapes/round1.exr.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bbo5hotdg6mg7" +path="res://.godot/imported/round1.exr-8050cfbed31968e6ce8bd055fbaa6897.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/zylann.hterrain/tools/brush/shapes/round1.exr" +dest_files=["res://.godot/imported/round1.exr-8050cfbed31968e6ce8bd055fbaa6897.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/addons/zylann.hterrain/tools/brush/shapes/round2.exr b/game/addons/zylann.hterrain/tools/brush/shapes/round2.exr Binary files differnew file mode 100644 index 0000000..477ab7e --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/shapes/round2.exr diff --git a/game/addons/zylann.hterrain/tools/brush/shapes/round2.exr.import b/game/addons/zylann.hterrain/tools/brush/shapes/round2.exr.import new file mode 100644 index 0000000..6038a8e --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/shapes/round2.exr.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://tn1ww3c47pwy" +path="res://.godot/imported/round2.exr-2a843db3bf131f2b2f5964ce65600f42.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/zylann.hterrain/tools/brush/shapes/round2.exr" +dest_files=["res://.godot/imported/round2.exr-2a843db3bf131f2b2f5964ce65600f42.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/addons/zylann.hterrain/tools/brush/shapes/round3.exr b/game/addons/zylann.hterrain/tools/brush/shapes/round3.exr Binary files differnew file mode 100644 index 0000000..b466f92 --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/shapes/round3.exr diff --git a/game/addons/zylann.hterrain/tools/brush/shapes/round3.exr.import b/game/addons/zylann.hterrain/tools/brush/shapes/round3.exr.import new file mode 100644 index 0000000..e8f2107 --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/shapes/round3.exr.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://baim7e27k13r4" +path="res://.godot/imported/round3.exr-77a9cdd9a592eb6010dc1db702d42c3a.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/zylann.hterrain/tools/brush/shapes/round3.exr" +dest_files=["res://.godot/imported/round3.exr-77a9cdd9a592eb6010dc1db702d42c3a.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/addons/zylann.hterrain/tools/brush/shapes/smoke.exr b/game/addons/zylann.hterrain/tools/brush/shapes/smoke.exr Binary files differnew file mode 100644 index 0000000..021947b --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/shapes/smoke.exr diff --git a/game/addons/zylann.hterrain/tools/brush/shapes/smoke.exr.import b/game/addons/zylann.hterrain/tools/brush/shapes/smoke.exr.import new file mode 100644 index 0000000..85151ff --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/shapes/smoke.exr.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cl6pxk6wr4hem" +path="res://.godot/imported/smoke.exr-0061a0a2acdf1ca295ec547e4b8c920d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/zylann.hterrain/tools/brush/shapes/smoke.exr" +dest_files=["res://.godot/imported/smoke.exr-0061a0a2acdf1ca295ec547e4b8c920d.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/addons/zylann.hterrain/tools/brush/shapes/texture1.exr b/game/addons/zylann.hterrain/tools/brush/shapes/texture1.exr Binary files differnew file mode 100644 index 0000000..f456b77 --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/shapes/texture1.exr diff --git a/game/addons/zylann.hterrain/tools/brush/shapes/texture1.exr.import b/game/addons/zylann.hterrain/tools/brush/shapes/texture1.exr.import new file mode 100644 index 0000000..9226d03 --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/shapes/texture1.exr.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dqdkxiq52oo0j" +path="res://.godot/imported/texture1.exr-0fac1840855f814972ea5666743101fc.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/zylann.hterrain/tools/brush/shapes/texture1.exr" +dest_files=["res://.godot/imported/texture1.exr-0fac1840855f814972ea5666743101fc.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/addons/zylann.hterrain/tools/brush/shapes/thing.exr b/game/addons/zylann.hterrain/tools/brush/shapes/thing.exr Binary files differnew file mode 100644 index 0000000..49d341e --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/shapes/thing.exr diff --git a/game/addons/zylann.hterrain/tools/brush/shapes/thing.exr.import b/game/addons/zylann.hterrain/tools/brush/shapes/thing.exr.import new file mode 100644 index 0000000..90ba164 --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/shapes/thing.exr.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://blljfgdlwdae5" +path="res://.godot/imported/thing.exr-8e88d861fe83e5e870fa01faee694c73.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/zylann.hterrain/tools/brush/shapes/thing.exr" +dest_files=["res://.godot/imported/thing.exr-8e88d861fe83e5e870fa01faee694c73.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/addons/zylann.hterrain/tools/brush/shapes/vegetation1.exr b/game/addons/zylann.hterrain/tools/brush/shapes/vegetation1.exr Binary files differnew file mode 100644 index 0000000..d65bc6e --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/shapes/vegetation1.exr diff --git a/game/addons/zylann.hterrain/tools/brush/shapes/vegetation1.exr.import b/game/addons/zylann.hterrain/tools/brush/shapes/vegetation1.exr.import new file mode 100644 index 0000000..2a1c46a --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/shapes/vegetation1.exr.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ca5nk2h4ukm0g" +path="res://.godot/imported/vegetation1.exr-0573f4c73944e2dd8f3202b8930ac625.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/zylann.hterrain/tools/brush/shapes/vegetation1.exr" +dest_files=["res://.godot/imported/vegetation1.exr-0573f4c73944e2dd8f3202b8930ac625.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/addons/zylann.hterrain/tools/brush/terrain_painter.gd b/game/addons/zylann.hterrain/tools/brush/terrain_painter.gd new file mode 100644 index 0000000..81891b6 --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/terrain_painter.gd @@ -0,0 +1,573 @@ +@tool +extends Node + +const HT_Painter = preload("./painter.gd") +const HTerrain = preload("../../hterrain.gd") +const HTerrainData = preload("../../hterrain_data.gd") +const HT_Logger = preload("../../util/logger.gd") +const HT_Brush = preload("./brush.gd") + +const HT_RaiseShader = preload("./shaders/raise.gdshader") +const HT_SmoothShader = preload("./shaders/smooth.gdshader") +const HT_LevelShader = preload("./shaders/level.gdshader") +const HT_FlattenShader = preload("./shaders/flatten.gdshader") +const HT_ErodeShader = preload("./shaders/erode.gdshader") +const HT_Splat4Shader = preload("./shaders/splat4.gdshader") +const HT_Splat16Shader = preload("./shaders/splat16.gdshader") +const HT_SplatIndexedShader = preload("./shaders/splat_indexed.gdshader") +const HT_ColorShader = preload("./shaders/color.gdshader") +const HT_AlphaShader = preload("./shaders/alpha.gdshader") + +const MODE_RAISE = 0 +const MODE_LOWER = 1 +const MODE_SMOOTH = 2 +const MODE_FLATTEN = 3 +const MODE_SPLAT = 4 +const MODE_COLOR = 5 +const MODE_MASK = 6 +const MODE_DETAIL = 7 +const MODE_LEVEL = 8 +const MODE_ERODE = 9 +const MODE_COUNT = 10 + +class HT_ModifiedMap: + var map_type := 0 + var map_index := 0 + var painter_index := 0 + +signal flatten_height_changed + +var _painters : Array[HT_Painter] = [] + +var _brush := HT_Brush.new() + +var _color := Color(1, 0, 0, 1) +var _mask_flag := false +var _mode := MODE_RAISE +var _flatten_height := 0.0 +var _detail_index := 0 +var _detail_density := 1.0 +var _texture_index := 0 +var _slope_limit_low_angle := 0.0 +var _slope_limit_high_angle := PI / 2.0 + +var _modified_maps := [] +var _terrain : HTerrain +var _logger = HT_Logger.get_for(self) + + +func _init(): + for i in 4: + var p := HT_Painter.new() + # The name is just for debugging + p.set_name(str("Painter", i)) + #p.set_brush_size(_brush_size) + p.texture_region_changed.connect(_on_painter_texture_region_changed.bind(i)) + add_child(p) + _painters.append(p) + + +func get_brush() -> HT_Brush: + return _brush + + +func get_brush_size() -> int: + return _brush.get_size() + + +func set_brush_size(s: int): + _brush.set_size(s) +# for p in _painters: +# p.set_brush_size(_brush_size) + + +func set_brush_texture(texture: Texture2D): + _brush.set_shapes([texture]) +# for p in _painters: +# p.set_brush_texture(texture) + + +func get_opacity() -> float: + return _brush.get_opacity() + + +func set_opacity(opacity: float): + _brush.set_opacity(opacity) + + +func set_flatten_height(h: float): + if h == _flatten_height: + return + _flatten_height = h + flatten_height_changed.emit() + + +func get_flatten_height() -> float: + return _flatten_height + + +func set_color(c: Color): + _color = c + + +func get_color() -> Color: + return _color + + +func set_mask_flag(m: bool): + _mask_flag = m + + +func get_mask_flag() -> bool: + return _mask_flag + + +func set_detail_density(d: float): + _detail_density = clampf(d, 0.0, 1.0) + + +func get_detail_density() -> float: + return _detail_density + + +func set_detail_index(di: int): + _detail_index = di + + +func set_texture_index(i: int): + _texture_index = i + + +func get_texture_index() -> int: + return _texture_index + + +func get_slope_limit_low_angle() -> float: + return _slope_limit_low_angle + + +func get_slope_limit_high_angle() -> float: + return _slope_limit_high_angle + + +func set_slope_limit_angles(low: float, high: float): + _slope_limit_low_angle = low + _slope_limit_high_angle = high + + +func is_operation_pending() -> bool: + for p in _painters: + if p.is_operation_pending(): + return true + return false + + +func has_modified_chunks() -> bool: + for p in _painters: + if p.has_modified_chunks(): + return true + return false + + +func get_undo_chunk_size() -> int: + return HT_Painter.UNDO_CHUNK_SIZE + + +func commit() -> Dictionary: + assert(_terrain.get_data() != null) + var terrain_data = _terrain.get_data() + assert(not terrain_data.is_locked()) + + var changes := [] + var chunk_positions : Array + + assert(len(_modified_maps) > 0) + + for mm in _modified_maps: + #print("Flushing painter ", mm.painter_index) + var painter : HT_Painter = _painters[mm.painter_index] + var info := painter.commit() + + # Note, positions are always the same for each map + chunk_positions = info.chunk_positions + + changes.append({ + "map_type": mm.map_type, + "map_index": mm.map_index, + "chunk_initial_datas": info.chunk_initial_datas, + "chunk_final_datas": info.chunk_final_datas + }) + + var cs := get_undo_chunk_size() + for pos in info.chunk_positions: + var rect = Rect2(pos * cs, Vector2(cs, cs)) + # This will update vertical bounds and notify normal map baker, + # since the latter updates out of order for preview + terrain_data.notify_region_change(rect, mm.map_type, mm.map_index, false, true) + +# for i in len(_painters): +# var p = _painters[i] +# if p.has_modified_chunks(): +# print("Painter ", i, " has modified chunks") + + # `commit()` is supposed to consume these chunks, there should be none left + assert(not has_modified_chunks()) + + return { + "chunk_positions": chunk_positions, + "maps": changes + } + + +func set_mode(mode: int): + assert(mode >= 0 and mode < MODE_COUNT) + _mode = mode + + +func get_mode() -> int: + return _mode + + +func set_terrain(terrain: HTerrain): + if terrain == _terrain: + return + _terrain = terrain + # It's important to release resources here, + # otherwise Godot keeps modified terrain maps in memory and "reloads" them like that + # next time we reopen the scene, even if we didn't save it + for p in _painters: + p.set_image(null, null) + p.clear_brush_shader_params() + + +# This may be called from an `_input` callback. +# Returns `true` if any change was performed. +func paint_input(position: Vector2, pressure: float) -> bool: + assert(_terrain.get_data() != null) + var data := _terrain.get_data() + assert(not data.is_locked()) + + if not _brush.configure_paint_input(_painters, position, pressure): + # Sometimes painting may not happen due to frequency options + return false + + _modified_maps.clear() + + match _mode: + MODE_RAISE: + _paint_height(data, position, 1.0) + + MODE_LOWER: + _paint_height(data, position, -1.0) + + MODE_SMOOTH: + _paint_smooth(data, position) + + MODE_FLATTEN: + _paint_flatten(data, position) + + MODE_LEVEL: + _paint_level(data, position) + + MODE_ERODE: + _paint_erode(data, position) + + MODE_SPLAT: + # TODO Properly support what happens when painting outside of supported index + # var supported_slots_count := terrain.get_cached_ground_texture_slot_count() + # if _texture_index >= supported_slots_count: + # _logger.debug("Painting out of range of supported texture slots: {0}/{1}" \ + # .format([_texture_index, supported_slots_count])) + # return + if _terrain.is_using_indexed_splatmap(): + _paint_splat_indexed(data, position) + else: + var splatmap_count := _terrain.get_used_splatmaps_count() + match splatmap_count: + 1: + _paint_splat4(data, position) + 4: + _paint_splat16(data, position) + + MODE_COLOR: + _paint_color(data, position) + + MODE_MASK: + _paint_mask(data, position) + + MODE_DETAIL: + _paint_detail(data, position) + + _: + _logger.error("Unknown mode {0}".format([_mode])) + + assert(len(_modified_maps) > 0) + return true + + +func _on_painter_texture_region_changed(rect: Rect2, painter_index: int): + var data := _terrain.get_data() + if data == null: + return + for mm in _modified_maps: + if mm.painter_index == painter_index: + # This will tell auto-baked maps to update (like normals). + data.notify_region_change(rect, mm.map_type, mm.map_index, false, false) + break + + +func _paint_height(data: HTerrainData, position: Vector2, factor: float): + var image := data.get_image(HTerrainData.CHANNEL_HEIGHT) + var texture := data.get_texture(HTerrainData.CHANNEL_HEIGHT, 0, true) + + var mm := HT_ModifiedMap.new() + mm.map_type = HTerrainData.CHANNEL_HEIGHT + mm.map_index = 0 + mm.painter_index = 0 + _modified_maps = [mm] + + # When using sculpting tools, make it dependent on brush size + var raise_strength := 10.0 + float(_brush.get_size()) + var delta := factor * (2.0 / 60.0) * raise_strength + + var p : HT_Painter = _painters[0] + + p.set_brush_shader(HT_RaiseShader) + p.set_brush_shader_param("u_factor", delta) + p.set_image(image, texture) + p.paint_input(position) + + +func _paint_smooth(data: HTerrainData, position: Vector2): + var image := data.get_image(HTerrainData.CHANNEL_HEIGHT) + var texture := data.get_texture(HTerrainData.CHANNEL_HEIGHT, 0, true) + + var mm := HT_ModifiedMap.new() + mm.map_type = HTerrainData.CHANNEL_HEIGHT + mm.map_index = 0 + mm.painter_index = 0 + _modified_maps = [mm] + + var p : HT_Painter = _painters[0] + + p.set_brush_shader(HT_SmoothShader) + p.set_brush_shader_param("u_factor", 1.0) + p.set_image(image, texture) + p.paint_input(position) + + +func _paint_flatten(data: HTerrainData, position: Vector2): + var image := data.get_image(HTerrainData.CHANNEL_HEIGHT) + var texture := data.get_texture(HTerrainData.CHANNEL_HEIGHT, 0, true) + + var mm := HT_ModifiedMap.new() + mm.map_type = HTerrainData.CHANNEL_HEIGHT + mm.map_index = 0 + mm.painter_index = 0 + _modified_maps = [mm] + + var p : HT_Painter = _painters[0] + + p.set_brush_shader(HT_FlattenShader) + p.set_brush_shader_param("u_flatten_value", _flatten_height) + p.set_image(image, texture) + p.paint_input(position) + + +func _paint_level(data: HTerrainData, position: Vector2): + var image := data.get_image(HTerrainData.CHANNEL_HEIGHT) + var texture := data.get_texture(HTerrainData.CHANNEL_HEIGHT, 0, true) + + var mm := HT_ModifiedMap.new() + mm.map_type = HTerrainData.CHANNEL_HEIGHT + mm.map_index = 0 + mm.painter_index = 0 + _modified_maps = [mm] + + var p : HT_Painter = _painters[0] + + p.set_brush_shader(HT_LevelShader) + p.set_brush_shader_param("u_factor", (10.0 / 60.0)) + p.set_image(image, texture) + p.paint_input(position) + + +func _paint_erode(data: HTerrainData, position: Vector2): + var image := data.get_image(HTerrainData.CHANNEL_HEIGHT) + var texture := data.get_texture(HTerrainData.CHANNEL_HEIGHT, 0, true) + + var mm := HT_ModifiedMap.new() + mm.map_type = HTerrainData.CHANNEL_HEIGHT + mm.map_index = 0 + mm.painter_index = 0 + _modified_maps = [mm] + + var p : HT_Painter = _painters[0] + + p.set_brush_shader(HT_ErodeShader) + p.set_image(image, texture) + p.paint_input(position) + + +func _paint_splat4(data: HTerrainData, position: Vector2): + var image := data.get_image(HTerrainData.CHANNEL_SPLAT) + var texture := data.get_texture(HTerrainData.CHANNEL_SPLAT, 0, true) + var heightmap_texture := data.get_texture(HTerrainData.CHANNEL_HEIGHT, 0) + + var mm := HT_ModifiedMap.new() + mm.map_type = HTerrainData.CHANNEL_SPLAT + mm.map_index = 0 + mm.painter_index = 0 + _modified_maps = [mm] + + var p : HT_Painter = _painters[0] + var splat := Color(0.0, 0.0, 0.0, 0.0) + splat[_texture_index] = 1.0; + p.set_brush_shader(HT_Splat4Shader) + p.set_brush_shader_param("u_splat", splat) + _set_slope_limit_shader_params(p, heightmap_texture) + p.set_image(image, texture) + p.paint_input(position) + + +func _paint_splat_indexed(data: HTerrainData, position: Vector2): + var map_types := [ + HTerrainData.CHANNEL_SPLAT_INDEX, + HTerrainData.CHANNEL_SPLAT_WEIGHT + ] + _modified_maps = [] + + var textures := [] + for mode in 2: + textures.append(data.get_texture(map_types[mode], 0, true)) + + for mode in 2: + var image := data.get_image(map_types[mode]) + + var mm := HT_ModifiedMap.new() + mm.map_type = map_types[mode] + mm.map_index = 0 + mm.painter_index = mode + _modified_maps.append(mm) + + var p : HT_Painter = _painters[mode] + + p.set_brush_shader(HT_SplatIndexedShader) + p.set_brush_shader_param("u_mode", mode) + p.set_brush_shader_param("u_index_map", textures[0]) + p.set_brush_shader_param("u_weight_map", textures[1]) + p.set_brush_shader_param("u_texture_index", _texture_index) + p.set_image(image, textures[mode]) + p.paint_input(position) + + +func _paint_splat16(data: HTerrainData, position: Vector2): + # Make sure required maps are present + while data.get_map_count(HTerrainData.CHANNEL_SPLAT) < 4: + data._edit_add_map(HTerrainData.CHANNEL_SPLAT) + + var splats := [] + for i in 4: + splats.append(Color(0.0, 0.0, 0.0, 0.0)) + splats[_texture_index / 4][_texture_index % 4] = 1.0 + + var textures := [] + for i in 4: + textures.append(data.get_texture(HTerrainData.CHANNEL_SPLAT, i, true)) + + var heightmap_texture := data.get_texture(HTerrainData.CHANNEL_HEIGHT, 0) + + for i in 4: + var image : Image = data.get_image(HTerrainData.CHANNEL_SPLAT, i) + var texture : Texture = textures[i] + + var mm := HT_ModifiedMap.new() + mm.map_type = HTerrainData.CHANNEL_SPLAT + mm.map_index = i + mm.painter_index = i + _modified_maps.append(mm) + + var p : HT_Painter = _painters[i] + + var other_splatmaps := [] + for tex in textures: + if tex != texture: + other_splatmaps.append(tex) + + p.set_brush_shader(HT_Splat16Shader) + p.set_brush_shader_param("u_splat", splats[i]) + p.set_brush_shader_param("u_other_splatmap_1", other_splatmaps[0]) + p.set_brush_shader_param("u_other_splatmap_2", other_splatmaps[1]) + p.set_brush_shader_param("u_other_splatmap_3", other_splatmaps[2]) + _set_slope_limit_shader_params(p, heightmap_texture) + p.set_image(image, texture) + p.paint_input(position) + + +func _paint_color(data: HTerrainData, position: Vector2): + var image := data.get_image(HTerrainData.CHANNEL_COLOR) + var texture := data.get_texture(HTerrainData.CHANNEL_COLOR, 0, true) + + var mm := HT_ModifiedMap.new() + mm.map_type = HTerrainData.CHANNEL_COLOR + mm.map_index = 0 + mm.painter_index = 0 + _modified_maps = [mm] + + var p : HT_Painter = _painters[0] + + # There was a problem with painting colors because of sRGB + # https://github.com/Zylann/godot_heightmap_plugin/issues/17#issuecomment-734001879 + + p.set_brush_shader(HT_ColorShader) + p.set_brush_shader_param("u_color", _color) + p.set_brush_shader_param("u_normal_min_y", 0.0) + p.set_brush_shader_param("u_normal_max_y", 1.0) + p.set_image(image, texture) + p.paint_input(position) + + +func _paint_mask(data: HTerrainData, position: Vector2): + var image := data.get_image(HTerrainData.CHANNEL_COLOR) + var texture := data.get_texture(HTerrainData.CHANNEL_COLOR, 0, true) + + var mm := HT_ModifiedMap.new() + mm.map_type = HTerrainData.CHANNEL_COLOR + mm.map_index = 0 + mm.painter_index = 0 + _modified_maps = [mm] + + var p : HT_Painter = _painters[0] + + p.set_brush_shader(HT_AlphaShader) + p.set_brush_shader_param("u_value", 1.0 if _mask_flag else 0.0) + p.set_image(image, texture) + p.paint_input(position) + + +func _paint_detail(data: HTerrainData, position: Vector2): + var image := data.get_image(HTerrainData.CHANNEL_DETAIL, _detail_index) + var texture := data.get_texture(HTerrainData.CHANNEL_DETAIL, _detail_index, true) + var heightmap_texture = data.get_texture(HTerrainData.CHANNEL_HEIGHT, 0) + + var mm := HT_ModifiedMap.new() + mm.map_type = HTerrainData.CHANNEL_DETAIL + mm.map_index = _detail_index + mm.painter_index = 0 + _modified_maps = [mm] + + var p : HT_Painter = _painters[0] + var c := Color(_detail_density, _detail_density, _detail_density, 1.0) + + # TODO Don't use this shader (why?) + p.set_brush_shader(HT_ColorShader) + p.set_brush_shader_param("u_color", c) + _set_slope_limit_shader_params(p, heightmap_texture) + p.set_image(image, texture) + p.paint_input(position) + + +func _set_slope_limit_shader_params(p: HT_Painter, heightmap_texture: Texture): + p.set_brush_shader_param("u_normal_min_y", cos(_slope_limit_high_angle)) + p.set_brush_shader_param("u_normal_max_y", cos(_slope_limit_low_angle) + 0.001) + p.set_brush_shader_param("u_heightmap", heightmap_texture) |