first commit

This commit is contained in:
Ugric
2026-03-02 02:17:04 +00:00
commit 5d56860f3a
813 changed files with 41799 additions and 0 deletions

View 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

View File

@@ -0,0 +1 @@
uid://b1fa3v8p8vb0u

View 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)

View File

@@ -0,0 +1 @@
uid://dkj3dnr8pntpi

View 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"]

View 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)

View File

@@ -0,0 +1 @@
uid://dcd854gty4alr

View 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;
}

View File

@@ -0,0 +1 @@
uid://bgd1dobh61btl

View File

@@ -0,0 +1,6 @@
shader_type canvas_item;
render_mode blend_disabled;
void fragment() {
COLOR = texture(TEXTURE, UV);
}

View File

@@ -0,0 +1 @@
uid://b4mcoo6bhjqmn

View 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);
}

View File

@@ -0,0 +1 @@
uid://ymyi13ox3pw7

View 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

View File

@@ -0,0 +1 @@
uid://d28lmha7aji0l

View 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()

View File

@@ -0,0 +1 @@
uid://b1b35q1fijabj

View File

@@ -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"]

View 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)

View File

@@ -0,0 +1 @@
uid://mbww7v2v25nn

View 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)

View File

@@ -0,0 +1 @@
uid://1xc5c77jq82e

View File

@@ -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

View 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));
}

View File

@@ -0,0 +1 @@
uid://ctyfdcpwn7u1h

View 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;
}

View File

@@ -0,0 +1 @@
uid://cm5sx634mgyfw

View 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);
}

View File

@@ -0,0 +1 @@
uid://c42imxjqfdrbc

View 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);
}

View File

@@ -0,0 +1 @@
uid://bjv4wyrnk5ve6

View 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);
}

View File

@@ -0,0 +1 @@
uid://coke71n64caxu

View 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);
}

View File

@@ -0,0 +1 @@
uid://ckob4h0qetlwr

View 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);
}

View File

@@ -0,0 +1 @@
uid://soc6v0vp5q57

View 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;
}

View File

@@ -0,0 +1 @@
uid://b28x6nh72ksgb

View 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;
}

View File

@@ -0,0 +1 @@
uid://imbkuc1meusn

View 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);
}
}

View File

@@ -0,0 +1 @@
uid://ccj38q8toifpu

Binary file not shown.

View File

@@ -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

Binary file not shown.

View 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

Binary file not shown.

View 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

Binary file not shown.

View 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

Binary file not shown.

View 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

Binary file not shown.

View 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

Binary file not shown.

View File

@@ -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

Binary file not shown.

View 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

Binary file not shown.

View File

@@ -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

View 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)

View File

@@ -0,0 +1 @@
uid://bf33os3ieoxxj