first commit
This commit is contained in:
217
addons/zylann.hterrain/tools/brush/brush.gd
Executable file
217
addons/zylann.hterrain/tools/brush/brush.gd
Executable file
@@ -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
|
||||
|
||||
|
||||
1
addons/zylann.hterrain/tools/brush/brush.gd.uid
Normal file
1
addons/zylann.hterrain/tools/brush/brush.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b1fa3v8p8vb0u
|
||||
234
addons/zylann.hterrain/tools/brush/brush_editor.gd
Executable file
234
addons/zylann.hterrain/tools/brush/brush_editor.gd
Executable file
@@ -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)
|
||||
1
addons/zylann.hterrain/tools/brush/brush_editor.gd.uid
Normal file
1
addons/zylann.hterrain/tools/brush/brush_editor.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dkj3dnr8pntpi
|
||||
130
addons/zylann.hterrain/tools/brush/brush_editor.tscn
Normal file
130
addons/zylann.hterrain/tools/brush/brush_editor.tscn
Normal file
@@ -0,0 +1,130 @@
|
||||
[gd_scene load_steps=4 format=3 uid="uid://bd42ig216p216"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dkj3dnr8pntpi" path="res://addons/zylann.hterrain/tools/brush/brush_editor.gd" id="1"]
|
||||
[ext_resource type="Script" uid="uid://dnqvfdgwxl85k" 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"]
|
||||
121
addons/zylann.hterrain/tools/brush/decal.gd
Executable file
121
addons/zylann.hterrain/tools/brush/decal.gd
Executable file
@@ -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)
|
||||
|
||||
1
addons/zylann.hterrain/tools/brush/decal.gd.uid
Normal file
1
addons/zylann.hterrain/tools/brush/decal.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dcd854gty4alr
|
||||
41
addons/zylann.hterrain/tools/brush/decal.gdshader
Executable file
41
addons/zylann.hterrain/tools/brush/decal.gdshader
Executable file
@@ -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;
|
||||
}
|
||||
1
addons/zylann.hterrain/tools/brush/decal.gdshader.uid
Normal file
1
addons/zylann.hterrain/tools/brush/decal.gdshader.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bgd1dobh61btl
|
||||
6
addons/zylann.hterrain/tools/brush/no_blend.gdshader
Executable file
6
addons/zylann.hterrain/tools/brush/no_blend.gdshader
Executable file
@@ -0,0 +1,6 @@
|
||||
shader_type canvas_item;
|
||||
render_mode blend_disabled;
|
||||
|
||||
void fragment() {
|
||||
COLOR = texture(TEXTURE, UV);
|
||||
}
|
||||
1
addons/zylann.hterrain/tools/brush/no_blend.gdshader.uid
Normal file
1
addons/zylann.hterrain/tools/brush/no_blend.gdshader.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b4mcoo6bhjqmn
|
||||
9
addons/zylann.hterrain/tools/brush/no_blend_rf.gdshader
Executable file
9
addons/zylann.hterrain/tools/brush/no_blend_rf.gdshader
Executable file
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://ymyi13ox3pw7
|
||||
399
addons/zylann.hterrain/tools/brush/painter.gd
Executable file
399
addons/zylann.hterrain/tools/brush/painter.gd
Executable file
@@ -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
|
||||
1
addons/zylann.hterrain/tools/brush/painter.gd.uid
Normal file
1
addons/zylann.hterrain/tools/brush/painter.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://d28lmha7aji0l
|
||||
280
addons/zylann.hterrain/tools/brush/settings_dialog/brush_settings_dialog.gd
Executable file
280
addons/zylann.hterrain/tools/brush/settings_dialog/brush_settings_dialog.gd
Executable file
@@ -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()
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://b1b35q1fijabj
|
||||
@@ -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" uid="uid://b1b35q1fijabj" 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"]
|
||||
41
addons/zylann.hterrain/tools/brush/settings_dialog/preview_painter.gd
Executable file
41
addons/zylann.hterrain/tools/brush/settings_dialog/preview_painter.gd
Executable file
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://mbww7v2v25nn
|
||||
70
addons/zylann.hterrain/tools/brush/settings_dialog/preview_scratchpad.gd
Executable file
70
addons/zylann.hterrain/tools/brush/settings_dialog/preview_scratchpad.gd
Executable file
@@ -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)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://1xc5c77jq82e
|
||||
@@ -0,0 +1,22 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://ng00jipfeucy"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://1xc5c77jq82e" path="res://addons/zylann.hterrain/tools/brush/settings_dialog/preview_scratchpad.gd" id="1"]
|
||||
[ext_resource type="Script" uid="uid://mbww7v2v25nn" 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
|
||||
19
addons/zylann.hterrain/tools/brush/shaders/alpha.gdshader
Executable file
19
addons/zylann.hterrain/tools/brush/shaders/alpha.gdshader
Executable file
@@ -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));
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://ctyfdcpwn7u1h
|
||||
68
addons/zylann.hterrain/tools/brush/shaders/color.gdshader
Executable file
68
addons/zylann.hterrain/tools/brush/shaders/color.gdshader
Executable file
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://cm5sx634mgyfw
|
||||
64
addons/zylann.hterrain/tools/brush/shaders/erode.gdshader
Executable file
64
addons/zylann.hterrain/tools/brush/shaders/erode.gdshader
Executable file
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://c42imxjqfdrbc
|
||||
22
addons/zylann.hterrain/tools/brush/shaders/flatten.gdshader
Executable file
22
addons/zylann.hterrain/tools/brush/shaders/flatten.gdshader
Executable file
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://bjv4wyrnk5ve6
|
||||
45
addons/zylann.hterrain/tools/brush/shaders/level.gdshader
Executable file
45
addons/zylann.hterrain/tools/brush/shaders/level.gdshader
Executable file
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://coke71n64caxu
|
||||
22
addons/zylann.hterrain/tools/brush/shaders/raise.gdshader
Executable file
22
addons/zylann.hterrain/tools/brush/shaders/raise.gdshader
Executable file
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://ckob4h0qetlwr
|
||||
34
addons/zylann.hterrain/tools/brush/shaders/smooth.gdshader
Executable file
34
addons/zylann.hterrain/tools/brush/shaders/smooth.gdshader
Executable file
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://soc6v0vp5q57
|
||||
81
addons/zylann.hterrain/tools/brush/shaders/splat16.gdshader
Executable file
81
addons/zylann.hterrain/tools/brush/shaders/splat16.gdshader
Executable file
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://b28x6nh72ksgb
|
||||
63
addons/zylann.hterrain/tools/brush/shaders/splat4.gdshader
Executable file
63
addons/zylann.hterrain/tools/brush/shaders/splat4.gdshader
Executable file
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://imbkuc1meusn
|
||||
89
addons/zylann.hterrain/tools/brush/shaders/splat_indexed.gdshader
Executable file
89
addons/zylann.hterrain/tools/brush/shaders/splat_indexed.gdshader
Executable file
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://ccj38q8toifpu
|
||||
BIN
addons/zylann.hterrain/tools/brush/shapes/acrylic1.exr
Executable file
BIN
addons/zylann.hterrain/tools/brush/shapes/acrylic1.exr
Executable file
Binary file not shown.
@@ -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
|
||||
BIN
addons/zylann.hterrain/tools/brush/shapes/round0.exr
Executable file
BIN
addons/zylann.hterrain/tools/brush/shapes/round0.exr
Executable file
Binary file not shown.
34
addons/zylann.hterrain/tools/brush/shapes/round0.exr.import
Normal file
34
addons/zylann.hterrain/tools/brush/shapes/round0.exr.import
Normal file
@@ -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
|
||||
BIN
addons/zylann.hterrain/tools/brush/shapes/round1.exr
Executable file
BIN
addons/zylann.hterrain/tools/brush/shapes/round1.exr
Executable file
Binary file not shown.
34
addons/zylann.hterrain/tools/brush/shapes/round1.exr.import
Normal file
34
addons/zylann.hterrain/tools/brush/shapes/round1.exr.import
Normal file
@@ -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
|
||||
BIN
addons/zylann.hterrain/tools/brush/shapes/round2.exr
Executable file
BIN
addons/zylann.hterrain/tools/brush/shapes/round2.exr
Executable file
Binary file not shown.
34
addons/zylann.hterrain/tools/brush/shapes/round2.exr.import
Normal file
34
addons/zylann.hterrain/tools/brush/shapes/round2.exr.import
Normal file
@@ -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
|
||||
BIN
addons/zylann.hterrain/tools/brush/shapes/round3.exr
Executable file
BIN
addons/zylann.hterrain/tools/brush/shapes/round3.exr
Executable file
Binary file not shown.
34
addons/zylann.hterrain/tools/brush/shapes/round3.exr.import
Normal file
34
addons/zylann.hterrain/tools/brush/shapes/round3.exr.import
Normal file
@@ -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
|
||||
BIN
addons/zylann.hterrain/tools/brush/shapes/smoke.exr
Executable file
BIN
addons/zylann.hterrain/tools/brush/shapes/smoke.exr
Executable file
Binary file not shown.
34
addons/zylann.hterrain/tools/brush/shapes/smoke.exr.import
Normal file
34
addons/zylann.hterrain/tools/brush/shapes/smoke.exr.import
Normal file
@@ -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
|
||||
BIN
addons/zylann.hterrain/tools/brush/shapes/texture1.exr
Executable file
BIN
addons/zylann.hterrain/tools/brush/shapes/texture1.exr
Executable file
Binary file not shown.
@@ -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
|
||||
BIN
addons/zylann.hterrain/tools/brush/shapes/thing.exr
Executable file
BIN
addons/zylann.hterrain/tools/brush/shapes/thing.exr
Executable file
Binary file not shown.
34
addons/zylann.hterrain/tools/brush/shapes/thing.exr.import
Normal file
34
addons/zylann.hterrain/tools/brush/shapes/thing.exr.import
Normal file
@@ -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
|
||||
BIN
addons/zylann.hterrain/tools/brush/shapes/vegetation1.exr
Executable file
BIN
addons/zylann.hterrain/tools/brush/shapes/vegetation1.exr
Executable file
Binary file not shown.
@@ -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
|
||||
573
addons/zylann.hterrain/tools/brush/terrain_painter.gd
Executable file
573
addons/zylann.hterrain/tools/brush/terrain_painter.gd
Executable file
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bf33os3ieoxxj
|
||||
Reference in New Issue
Block a user