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,28 @@
@tool
extends AcceptDialog
const HT_Util = preload("../../util/util.gd")
const HT_Logger = preload("../../util/logger.gd")
const HT_Errors = preload("../../util/errors.gd")
const PLUGIN_CFG_PATH = "res://addons/zylann.hterrain/plugin.cfg"
@onready var _about_rich_text_label : RichTextLabel = $VB/HB2/TC/About
var _logger = HT_Logger.get_for(self)
func _ready():
if HT_Util.is_in_edited_scene(self):
return
var plugin_cfg = ConfigFile.new()
var err := plugin_cfg.load(PLUGIN_CFG_PATH)
if err != OK:
_logger.error("Could not load {0}: {1}" \
.format([PLUGIN_CFG_PATH, HT_Errors.get_message(err)]))
return
var version = plugin_cfg.get_value("plugin", "version", "--.--.--")
_about_rich_text_label.text = _about_rich_text_label.text.format({"version": version})

View File

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

View File

@@ -0,0 +1,85 @@
[gd_scene load_steps=4 format=3 uid="uid://cvuubd08805oa"]
[ext_resource type="Script" uid="uid://dybp2pjggdr0d" path="res://addons/zylann.hterrain/tools/about/about_dialog.gd" id="1"]
[ext_resource type="Texture2D" uid="uid://sdaddk8wxjin" path="res://addons/zylann.hterrain/tools/icons/icon_heightmap_node_large.svg" id="2"]
[ext_resource type="Script" uid="uid://ict65vmutips" path="res://addons/zylann.hterrain/tools/util/rich_text_label_hyperlinks.gd" id="3"]
[node name="AboutDialog" type="AcceptDialog"]
size = Vector2i(516, 357)
script = ExtResource("1")
[node name="VB" type="VBoxContainer" parent="."]
custom_minimum_size = Vector2(500, 300)
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="HB2" type="HBoxContainer" parent="VB"]
layout_mode = 2
size_flags_vertical = 3
[node name="TextureRect" type="TextureRect" parent="VB/HB2"]
layout_mode = 2
texture = ExtResource("2")
stretch_mode = 2
[node name="TC" type="TabContainer" parent="VB/HB2"]
layout_mode = 2
size_flags_horizontal = 3
[node name="About" type="RichTextLabel" parent="VB/HB2/TC"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
bbcode_enabled = true
text = "Version: {version}
Author: Marc Gilleron
Repository: https://github.com/Zylann/godot_heightmap_plugin
Issue tracker: https://github.com/Zylann/godot_heightmap_plugin/issues
Gold supporters:
Aaron Franke (aaronfranke)
Silver supporters:
TheConceptBoy
Chris Bolton (yochrisbolton)
Gamerfiend (Snowminx)
greenlion (Justin Swanhart)
segfault-god (jp.owo.Manda)
RonanZe
Phyronnaz
NoFr1ends (Lynx)
Bronze supporters:
rcorre (Ryan Roden-Corrent)
duchainer (Raphaël Duchaîne)
MadMartian
stackdump (stackdump.eth)
Treer
MrGreaterThan
lenis0012
"
script = ExtResource("3")
[node name="License" type="RichTextLabel" parent="VB/HB2/TC"]
visible = false
layout_mode = 2
text = "Copyright (c) 2016-2023 Marc Gilleron
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"
[node name="HB" type="HBoxContainer" parent="VB"]
layout_mode = 2
alignment = 1

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

View File

@@ -0,0 +1,25 @@
shader_type canvas_item;
#include "res://addons/zylann.hterrain/shaders/include/heightmap.gdshaderinc"
vec4 pack_normal(vec3 n) {
return vec4((0.5 * (n + 1.0)).xzy, 1.0);
}
float get_height(sampler2D tex, vec2 uv) {
return sample_heightmap(tex, uv);
}
void fragment() {
vec2 uv = UV;
vec2 ps = TEXTURE_PIXEL_SIZE;
float left = get_height(TEXTURE, uv + vec2(-ps.x, 0));
float right = get_height(TEXTURE, uv + vec2(ps.x, 0));
float back = get_height(TEXTURE, uv + vec2(0, -ps.y));
float fore = get_height(TEXTURE, uv + vec2(0, ps.y));
vec3 n = normalize(vec3(left - right, 2.0, fore - back));
COLOR = pack_normal(n);
// DEBUG
//COLOR.r = fract(TIME * 100.0);
}

View File

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

View File

@@ -0,0 +1,200 @@
@tool
extends Control
const HTerrain = preload("../../hterrain.gd")
const HTerrainData = preload("../../hterrain_data.gd")
const HTerrainDetailLayer = preload("../../hterrain_detail_layer.gd")
const HT_ImageFileCache = preload("../../util/image_file_cache.gd")
const HT_EditorUtil = preload("../util/editor_util.gd")
const HT_Logger = preload("../../util/logger.gd")
# TODO Can't preload because it causes the plugin to fail loading if assets aren't imported
const PLACEHOLDER_ICON_TEXTURE = "res://addons/zylann.hterrain/tools/icons/icon_grass.svg"
const DETAIL_LAYER_ICON_TEXTURE = \
"res://addons/zylann.hterrain/tools/icons/icon_detail_layer_node.svg"
signal detail_selected(index)
# Emitted when the tool added or removed a detail map
signal detail_list_changed
@onready var _item_list : ItemList = $ItemList
@onready var _confirmation_dialog : ConfirmationDialog = $ConfirmationDialog
var _terrain : HTerrain = null
var _dialog_target := -1
var _undo_redo_manager : EditorUndoRedoManager
var _image_cache : HT_ImageFileCache
var _logger = HT_Logger.get_for(self)
func set_terrain(terrain):
if _terrain == terrain:
return
_terrain = terrain
_update_list()
func set_undo_redo(ur: EditorUndoRedoManager):
assert(ur != null)
_undo_redo_manager = ur
func set_image_cache(image_cache: HT_ImageFileCache):
_image_cache = image_cache
func set_layer_index(i: int):
_item_list.select(i, true)
func _update_list():
_item_list.clear()
if _terrain == null:
return
var layer_nodes = _terrain.get_detail_layers()
var layer_nodes_by_index := {}
for layer in layer_nodes:
if not layer_nodes_by_index.has(layer.layer_index):
layer_nodes_by_index[layer.layer_index] = []
layer_nodes_by_index[layer.layer_index].append(layer.name)
var data = _terrain.get_data()
if data != null:
# Display layers from what terrain data actually contains,
# because layer nodes are just what makes them rendered and aren't much restricted.
var layer_count = data.get_map_count(HTerrainData.CHANNEL_DETAIL)
var placeholder_icon = HT_EditorUtil.load_texture(PLACEHOLDER_ICON_TEXTURE, _logger)
for i in layer_count:
# TODO Show a preview icon
_item_list.add_item(str("Map ", i), placeholder_icon)
if layer_nodes_by_index.has(i):
# TODO How to keep names updated with node names?
var names := ", ".join(PackedStringArray(layer_nodes_by_index[i]))
if len(names) == 1:
_item_list.set_item_tooltip(i, "Used by " + names)
else:
_item_list.set_item_tooltip(i, "Used by " + names)
# Remove custom color
# TODO Use fg version when available in Godot 3.1, I want to only highlight text
_item_list.set_item_custom_bg_color(i, Color(0, 0, 0, 0))
else:
# TODO Use fg version when available in Godot 3.1, I want to only highlight text
_item_list.set_item_custom_bg_color(i, Color(1.0, 0.2, 0.2, 0.3))
_item_list.set_item_tooltip(i, "This map isn't used by any layer. " \
+ "Add a HTerrainDetailLayer node as child of the terrain.")
func _on_Add_pressed():
_add_layer()
func _on_Remove_pressed():
var selected = _item_list.get_selected_items()
if len(selected) == 0:
return
_dialog_target = _item_list.get_selected_items()[0]
_confirmation_dialog.title = "Removing detail map {0}".format([_dialog_target])
_confirmation_dialog.popup_centered()
func _on_ConfirmationDialog_confirmed():
_remove_layer(_dialog_target)
func _add_layer():
assert(_terrain != null)
assert(_terrain.get_data() != null)
assert(_undo_redo_manager != null)
var terrain_data : HTerrainData = _terrain.get_data()
# First, create node and map image
var node := HTerrainDetailLayer.new()
# TODO Workarounds for https://github.com/godotengine/godot/issues/21410
var detail_layer_icon := HT_EditorUtil.load_texture(DETAIL_LAYER_ICON_TEXTURE, _logger)
node.set_meta("_editor_icon", detail_layer_icon)
node.name = "HTerrainDetailLayer"
var map_index := terrain_data._edit_add_map(HTerrainData.CHANNEL_DETAIL)
var map_image := terrain_data.get_image(HTerrainData.CHANNEL_DETAIL)
var map_image_cache_id := _image_cache.save_image(map_image)
node.layer_index = map_index
var undo_redo := _undo_redo_manager.get_history_undo_redo(
_undo_redo_manager.get_object_history_id(_terrain))
# Then, create an action
undo_redo.create_action("Add Detail Layer {0}".format([map_index]))
undo_redo.add_do_method(terrain_data._edit_insert_map_from_image_cache.bind(
HTerrainData.CHANNEL_DETAIL, map_index, _image_cache, map_image_cache_id))
undo_redo.add_do_method(_terrain.add_child.bind(node))
undo_redo.add_do_property(node, "owner", get_tree().edited_scene_root)
undo_redo.add_do_method(self._update_list)
undo_redo.add_do_reference(node)
undo_redo.add_undo_method(_terrain.remove_child.bind(node))
undo_redo.add_undo_method(
terrain_data._edit_remove_map.bind(HTerrainData.CHANNEL_DETAIL, map_index))
undo_redo.add_undo_method(self._update_list)
# Yet another instance of this hack, to prevent UndoRedo from running some of the functions,
# which we had to run already
terrain_data._edit_set_disable_apply_undo(true)
undo_redo.commit_action()
terrain_data._edit_set_disable_apply_undo(false)
#_update_list()
detail_list_changed.emit()
var index := node.layer_index
_item_list.select(index)
# select() doesn't trigger the signal
detail_selected.emit(index)
func _remove_layer(map_index: int):
var terrain_data : HTerrainData = _terrain.get_data()
# First, cache image data
var image := terrain_data.get_image(HTerrainData.CHANNEL_DETAIL, map_index)
var image_id := _image_cache.save_image(image)
var nodes = _terrain.get_detail_layers()
var using_nodes := []
# Nodes using this map will be removed from the tree
for node in nodes:
if node.layer_index == map_index:
using_nodes.append(node)
var undo_redo := _undo_redo_manager.get_history_undo_redo(
_undo_redo_manager.get_object_history_id(_terrain))
undo_redo.create_action("Remove Detail Layer {0}".format([map_index]))
undo_redo.add_do_method(
terrain_data._edit_remove_map.bind(HTerrainData.CHANNEL_DETAIL, map_index))
for node in using_nodes:
undo_redo.add_do_method(_terrain.remove_child.bind(node))
undo_redo.add_do_method(self._update_list)
undo_redo.add_undo_method(terrain_data._edit_insert_map_from_image_cache.bind(
HTerrainData.CHANNEL_DETAIL, map_index, _image_cache, image_id))
for node in using_nodes:
undo_redo.add_undo_method(_terrain.add_child.bind(node))
undo_redo.add_undo_property(node, "owner", get_tree().edited_scene_root)
undo_redo.add_undo_reference(node)
undo_redo.add_undo_method(self._update_list)
undo_redo.commit_action()
#_update_list()
detail_list_changed.emit()
func _on_ItemList_item_selected(index):
detail_selected.emit(index)

View File

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

View File

@@ -0,0 +1,48 @@
[gd_scene load_steps=2 format=3 uid="uid://do3c3jse5p7hx"]
[ext_resource type="Script" uid="uid://cvy70epqlf1d8" path="res://addons/zylann.hterrain/tools/detail_editor/detail_editor.gd" id="1"]
[node name="DetailEditor" type="Control"]
custom_minimum_size = Vector2(200, 0)
layout_mode = 3
anchors_preset = 0
offset_right = 189.0
offset_bottom = 109.0
script = ExtResource("1")
[node name="ItemList" type="ItemList" parent="."]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
offset_bottom = -26.0
max_columns = 0
same_column_width = true
icon_mode = 0
fixed_icon_size = Vector2i(32, 32)
[node name="HBoxContainer" type="HBoxContainer" parent="."]
layout_mode = 0
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_top = -24.0
[node name="Add" type="Button" parent="HBoxContainer"]
layout_mode = 2
text = "Add"
[node name="Remove" type="Button" parent="HBoxContainer"]
layout_mode = 2
text = "Remove"
[node name="Label" type="Label" parent="HBoxContainer"]
layout_mode = 2
text = "Details"
[node name="ConfirmationDialog" type="ConfirmationDialog" parent="."]
dialog_text = "Are you sure you want to remove this detail map?"
[connection signal="item_selected" from="ItemList" to="." method="_on_ItemList_item_selected"]
[connection signal="pressed" from="HBoxContainer/Add" to="." method="_on_Add_pressed"]
[connection signal="pressed" from="HBoxContainer/Remove" to="." method="_on_Remove_pressed"]
[connection signal="confirmed" from="ConfirmationDialog" to="." method="_on_ConfirmationDialog_confirmed"]

View File

@@ -0,0 +1,221 @@
@tool
extends AcceptDialog
const HTerrain = preload("../../hterrain.gd")
const HTerrainData = preload("../../hterrain_data.gd")
const HT_Errors = preload("../../util/errors.gd")
const HT_Util = preload("../../util/util.gd")
const HT_Logger = preload("../../util/logger.gd")
const FORMAT_RH = 0
const FORMAT_RF = 1
const FORMAT_R16 = 2
const FORMAT_PNG8 = 3
const FORMAT_EXRH = 4
const FORMAT_EXRF = 5
const FORMAT_COUNT = 6
@onready var _output_path_line_edit := $VB/Grid/OutputPath/HeightmapPathLineEdit as LineEdit
@onready var _format_selector := $VB/Grid/FormatSelector as OptionButton
@onready var _height_range_min_spinbox := $VB/Grid/HeightRange/HeightRangeMin as SpinBox
@onready var _height_range_max_spinbox := $VB/Grid/HeightRange/HeightRangeMax as SpinBox
@onready var _export_button := $VB/Buttons/ExportButton as Button
@onready var _show_in_explorer_checkbox := $VB/ShowInExplorerCheckbox as CheckBox
var _terrain : HTerrain = null
var _file_dialog : EditorFileDialog = null
var _format_names := []
var _format_extensions := []
var _logger = HT_Logger.get_for(self)
func _init():
# Godot 4 decided to not have a plain WindowDialog class...
# there is Window but it's way too unfriendly...
get_ok_button().hide()
func _ready():
_format_names.resize(FORMAT_COUNT)
_format_extensions.resize(FORMAT_COUNT)
_format_names[FORMAT_RH] = "16-bit RAW float"
_format_names[FORMAT_RF] = "32-bit RAW float"
_format_names[FORMAT_R16] = "16-bit RAW unsigned"
_format_names[FORMAT_PNG8] = "8-bit PNG greyscale"
_format_names[FORMAT_EXRH] = "16-bit float greyscale EXR"
_format_names[FORMAT_EXRF] = "32-bit float greyscale EXR"
_format_extensions[FORMAT_RH] = "raw"
_format_extensions[FORMAT_RF] = "raw"
_format_extensions[FORMAT_R16] = "raw"
_format_extensions[FORMAT_PNG8] = "png"
_format_extensions[FORMAT_EXRH] = "exr"
_format_extensions[FORMAT_EXRF] = "exr"
if not HT_Util.is_in_edited_scene(self):
for i in len(_format_names):
_format_selector.get_popup().add_item(_format_names[i], i)
func setup_dialogs(base_control: Control):
assert(_file_dialog == null)
var fd := EditorFileDialog.new()
fd.file_mode = EditorFileDialog.FILE_MODE_SAVE_FILE
fd.unresizable = false
fd.access = EditorFileDialog.ACCESS_FILESYSTEM
fd.file_selected.connect(_on_FileDialog_file_selected)
add_child(fd)
_file_dialog = fd
_update_file_extension()
func set_terrain(terrain: HTerrain):
_terrain = terrain
func _exit_tree():
if _file_dialog != null:
_file_dialog.queue_free()
_file_dialog = null
func _on_FileDialog_file_selected(fpath: String):
_output_path_line_edit.text = fpath
func _auto_adjust_height_range():
assert(_terrain != null)
assert(_terrain.get_data() != null)
var aabb := _terrain.get_data().get_aabb()
_height_range_min_spinbox.value = aabb.position.y
_height_range_max_spinbox.value = aabb.position.y + aabb.size.y
func _export() -> bool:
assert(_terrain != null)
assert(_terrain.get_data() != null)
var src_heightmap: Image = _terrain.get_data().get_image(HTerrainData.CHANNEL_HEIGHT)
var fpath := _output_path_line_edit.text.strip_edges()
# TODO Is `selected` an ID or an index? I need an ID, it works by chance for now.
var format := _format_selector.selected
var height_min := _height_range_min_spinbox.value
var height_max := _height_range_max_spinbox.value
if height_min == height_max:
_logger.error("Cannot export, height range is zero")
return false
if height_min > height_max:
_logger.error("Cannot export, height min is greater than max")
return false
var save_error := OK
var float_heightmap := HTerrainData.convert_heightmap_to_float(src_heightmap, _logger)
if format == FORMAT_PNG8:
var hscale := 1.0 / (height_max - height_min)
var im := Image.create(
src_heightmap.get_width(), src_heightmap.get_height(), false, Image.FORMAT_R8)
for y in src_heightmap.get_height():
for x in src_heightmap.get_width():
var h := clampf((float_heightmap.get_pixel(x, y).r - height_min) * hscale, 0.0, 1.0)
im.set_pixel(x, y, Color(h, h, h))
save_error = im.save_png(fpath)
elif format == FORMAT_EXRH:
float_heightmap.convert(Image.FORMAT_RH)
save_error = float_heightmap.save_exr(fpath, true)
elif format == FORMAT_EXRF:
save_error = float_heightmap.save_exr(fpath, true)
else: # RAW
var f := FileAccess.open(fpath, FileAccess.WRITE)
if f == null:
var err := FileAccess.get_open_error()
_print_file_error(fpath, err)
return false
if format == FORMAT_RH:
float_heightmap.convert(Image.FORMAT_RH)
f.store_buffer(float_heightmap.get_data())
elif format == FORMAT_RF:
f.store_buffer(float_heightmap.get_data())
elif format == FORMAT_R16:
var hscale := 65535.0 / (height_max - height_min)
for y in float_heightmap.get_height():
for x in float_heightmap.get_width():
var h := int((float_heightmap.get_pixel(x, y).r - height_min) * hscale)
if h < 0:
h = 0
elif h > 65535:
h = 65535
if x % 50 == 0:
_logger.debug(str(h))
f.store_16(h)
if save_error == OK:
_logger.debug("Exported heightmap as \"{0}\"".format([fpath]))
return true
else:
_print_file_error(fpath, save_error)
return false
func _update_file_extension():
if _format_selector.selected == -1:
_format_selector.selected = 0
# This recursively calls the current function
return
# TODO Is `selected` an ID or an index? I need an ID, it works by chance for now.
var format = _format_selector.selected
var ext : String = _format_extensions[format]
_file_dialog.clear_filters()
_file_dialog.add_filter(str("*.", ext, " ; ", ext.to_upper(), " files"))
var fpath := _output_path_line_edit.text.strip_edges()
if fpath != "":
_output_path_line_edit.text = str(fpath.get_basename(), ".", ext)
func _print_file_error(fpath: String, err: int):
_logger.error("Could not save path {0}, error: {1}" \
.format([fpath, HT_Errors.get_message(err)]))
func _on_CancelButton_pressed():
hide()
func _on_ExportButton_pressed():
if _export():
hide()
if _show_in_explorer_checkbox.button_pressed:
OS.shell_open(_output_path_line_edit.text.strip_edges().get_base_dir())
func _on_HeightmapPathLineEdit_text_changed(new_text: String):
_export_button.disabled = (new_text.strip_edges() == "")
func _on_HeightmapPathBrowseButton_pressed():
_file_dialog.popup_centered_ratio()
func _on_FormatSelector_item_selected(id):
_update_file_extension()
func _on_HeightRangeAutoButton_pressed():
_auto_adjust_height_range()

View File

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

View File

@@ -0,0 +1,125 @@
[gd_scene load_steps=3 format=3 uid="uid://bcocysgmum5ag"]
[ext_resource type="Script" uid="uid://djipuv4g1yc7j" path="res://addons/zylann.hterrain/tools/exporter/export_image_dialog.gd" id="1"]
[ext_resource type="PackedScene" path="res://addons/zylann.hterrain/tools/util/dialog_fitter.tscn" id="2"]
[node name="ExportImageDialog" type="AcceptDialog"]
size = Vector2i(500, 340)
min_size = Vector2i(500, 250)
script = ExtResource("1")
[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 = -18.0
[node name="Grid" type="GridContainer" parent="VB"]
layout_mode = 2
columns = 2
[node name="OutputPathLabel" type="Label" parent="VB/Grid"]
layout_mode = 2
text = "Output path:"
[node name="OutputPath" type="HBoxContainer" parent="VB/Grid"]
layout_mode = 2
size_flags_horizontal = 3
[node name="HeightmapPathLineEdit" type="LineEdit" parent="VB/Grid/OutputPath"]
layout_mode = 2
size_flags_horizontal = 3
[node name="HeightmapPathBrowseButton" type="Button" parent="VB/Grid/OutputPath"]
layout_mode = 2
text = "..."
[node name="FormatLabel" type="Label" parent="VB/Grid"]
layout_mode = 2
text = "Format:"
[node name="FormatSelector" type="OptionButton" parent="VB/Grid"]
layout_mode = 2
[node name="HeightRangeLabel" type="Label" parent="VB/Grid"]
layout_mode = 2
text = "Height range:"
[node name="HeightRange" type="HBoxContainer" parent="VB/Grid"]
layout_mode = 2
[node name="Label" type="Label" parent="VB/Grid/HeightRange"]
layout_mode = 2
text = "Min"
[node name="HeightRangeMin" type="SpinBox" parent="VB/Grid/HeightRange"]
custom_minimum_size = Vector2(100, 0)
layout_mode = 2
min_value = -10000.0
max_value = 10000.0
step = 0.0
value = -2000.0
[node name="Label2" type="Label" parent="VB/Grid/HeightRange"]
layout_mode = 2
text = "Max"
[node name="HeightRangeMax" type="SpinBox" parent="VB/Grid/HeightRange"]
custom_minimum_size = Vector2(100, 0)
layout_mode = 2
min_value = -10000.0
max_value = 10000.0
step = 0.0
value = 2000.0
[node name="HeightRangeAutoButton" type="Button" parent="VB/Grid/HeightRange"]
layout_mode = 2
size_flags_horizontal = 3
text = "Auto"
[node name="ShowInExplorerCheckbox" type="CheckBox" parent="VB"]
layout_mode = 2
text = "Show in explorer after export"
[node name="Spacer" type="Control" parent="VB"]
custom_minimum_size = Vector2(0, 16)
layout_mode = 2
[node name="Label" type="Label" parent="VB"]
layout_mode = 2
text = "Note: height range is needed for integer image formats, as they can't directly represent the real height. 8-bit formats may cause precision loss."
autowrap_mode = 2
[node name="Spacer2" type="Control" parent="VB"]
custom_minimum_size = Vector2(0, 16)
layout_mode = 2
[node name="Buttons" type="HBoxContainer" parent="VB"]
layout_mode = 2
alignment = 1
[node name="ExportButton" type="Button" parent="VB/Buttons"]
layout_mode = 2
text = "Export"
[node name="CancelButton" type="Button" parent="VB/Buttons"]
layout_mode = 2
text = "Cancel"
[node name="DialogFitter" parent="." instance=ExtResource("2")]
layout_mode = 3
anchors_preset = 0
offset_left = 8.0
offset_top = 8.0
offset_right = 492.0
offset_bottom = 322.0
[connection signal="text_changed" from="VB/Grid/OutputPath/HeightmapPathLineEdit" to="." method="_on_HeightmapPathLineEdit_text_changed"]
[connection signal="pressed" from="VB/Grid/OutputPath/HeightmapPathBrowseButton" to="." method="_on_HeightmapPathBrowseButton_pressed"]
[connection signal="item_selected" from="VB/Grid/FormatSelector" to="." method="_on_FormatSelector_item_selected"]
[connection signal="pressed" from="VB/Grid/HeightRange/HeightRangeAutoButton" to="." method="_on_HeightRangeAutoButton_pressed"]
[connection signal="pressed" from="VB/Buttons/ExportButton" to="." method="_on_ExportButton_pressed"]
[connection signal="pressed" from="VB/Buttons/CancelButton" to="." method="_on_CancelButton_pressed"]

View File

@@ -0,0 +1,54 @@
@tool
extends AcceptDialog
signal generate_selected(lod)
const HTerrain = preload("../hterrain.gd")
const HTerrainMesher = preload("../hterrain_mesher.gd")
const HT_Util = preload("../util/util.gd")
@onready var _preview_label : Label = $VBoxContainer/PreviewLabel
@onready var _lod_spinbox : SpinBox = $VBoxContainer/HBoxContainer/LODSpinBox
var _terrain : HTerrain = null
func _init():
get_ok_button().hide()
func set_terrain(terrain: HTerrain):
_terrain = terrain
func _notification(what: int):
if what == NOTIFICATION_VISIBILITY_CHANGED:
if visible and _terrain != null:
_update_preview()
func _on_LODSpinBox_value_changed(value):
_update_preview()
func _update_preview():
assert(_terrain != null)
assert(_terrain.get_data() != null)
var resolution := _terrain.get_data().get_resolution()
var stride := int(_lod_spinbox.value)
resolution /= stride
var s := HTerrainMesher.get_mesh_size(resolution, resolution)
_preview_label.text = str( \
HT_Util.format_integer(s.vertices), " vertices, ", \
HT_Util.format_integer(s.triangles), " triangles")
func _on_Generate_pressed():
var stride := int(_lod_spinbox.value)
generate_selected.emit(stride)
hide()
func _on_Cancel_pressed():
hide()

View File

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

View File

@@ -0,0 +1,69 @@
[gd_scene load_steps=3 format=3 uid="uid://ci0da54goyo5o"]
[ext_resource type="Script" uid="uid://crfxt4lplnvbb" path="res://addons/zylann.hterrain/tools/generate_mesh_dialog.gd" id="1"]
[ext_resource type="PackedScene" uid="uid://hf4jllhtne1j" path="res://addons/zylann.hterrain/tools/util/dialog_fitter.tscn" id="2"]
[node name="GenerateMeshDialog" type="AcceptDialog"]
title = "Generate full mesh"
size = Vector2i(448, 234)
min_size = Vector2i(448, 186)
script = ExtResource("1")
[node name="VBoxContainer" 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 = -18.0
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
layout_mode = 2
[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer"]
layout_mode = 2
text = "LOD"
[node name="LODSpinBox" type="SpinBox" parent="VBoxContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
min_value = 1.0
max_value = 16.0
value = 1.0
[node name="PreviewLabel" type="Label" parent="VBoxContainer"]
layout_mode = 2
text = "9999 vertices, 9999 triangles"
[node name="Spacer" type="Control" parent="VBoxContainer"]
layout_mode = 2
[node name="Label" type="Label" parent="VBoxContainer"]
layout_mode = 2
text = "Note: generating a full mesh from the terrain may result in a huge amount of vertices for a single object. It is preferred to do this for small terrains, or as a temporary workaround to generate a navmesh."
autowrap_mode = 2
[node name="Buttons" type="HBoxContainer" parent="VBoxContainer"]
layout_mode = 2
alignment = 1
[node name="Generate" type="Button" parent="VBoxContainer/Buttons"]
layout_mode = 2
text = "Generate"
[node name="Cancel" type="Button" parent="VBoxContainer/Buttons"]
layout_mode = 2
text = "Cancel"
[node name="DialogFitter" parent="." instance=ExtResource("2")]
layout_mode = 3
anchors_preset = 0
offset_left = 8.0
offset_top = 8.0
offset_right = 440.0
offset_bottom = 216.0
[connection signal="value_changed" from="VBoxContainer/HBoxContainer/LODSpinBox" to="." method="_on_LODSpinBox_value_changed"]
[connection signal="pressed" from="VBoxContainer/Buttons/Generate" to="." method="_on_Generate_pressed"]
[connection signal="pressed" from="VBoxContainer/Buttons/Cancel" to="." method="_on_Cancel_pressed"]

View File

@@ -0,0 +1,562 @@
@tool
extends AcceptDialog
const HTerrain = preload("../../hterrain.gd")
const HTerrainData = preload("../../hterrain_data.gd")
const HTerrainMesher = preload("../../hterrain_mesher.gd")
const HT_Util = preload("../../util/util.gd")
const HT_TextureGenerator = preload("./texture_generator.gd")
const HT_TextureGeneratorPass = preload("./texture_generator_pass.gd")
const HT_Logger = preload("../../util/logger.gd")
const HT_ImageFileCache = preload("../../util/image_file_cache.gd")
const HT_Inspector = preload("../inspector/inspector.gd")
const HT_TerrainPreview = preload("../terrain_preview.gd")
const HT_ProgressWindow = preload("../progress_window.gd")
const HT_ProgressWindowScene = preload("../progress_window.tscn")
# TODO Power of two is assumed here.
# I wonder why it doesn't have the off by one terrain textures usually have
const MAX_VIEWPORT_RESOLUTION = 512
#signal progress_notified(info) # { "progress": real, "message": string, "finished": bool }
@onready var _inspector_container : Control = $VBoxContainer/Editor/Settings
@onready var _inspector : HT_Inspector = $VBoxContainer/Editor/Settings/Inspector
@onready var _preview : HT_TerrainPreview = $VBoxContainer/Editor/Preview/TerrainPreview
@onready var _progress_bar : ProgressBar = $VBoxContainer/Editor/Preview/ProgressBar
var _dummy_texture = load("res://addons/zylann.hterrain/tools/icons/empty.png")
var _terrain : HTerrain = null
var _applying := false
var _generator : HT_TextureGenerator
var _generated_textures := [null, null]
var _dialog_visible := false
var _undo_map_ids := {}
var _image_cache : HT_ImageFileCache = null
var _undo_redo_manager : EditorUndoRedoManager
var _logger := HT_Logger.get_for(self)
var _viewport_resolution := MAX_VIEWPORT_RESOLUTION
var _progress_window : HT_ProgressWindow
static func get_shader(shader_name: String) -> Shader:
var path := "res://addons/zylann.hterrain/tools/generator/shaders"\
.path_join(str(shader_name, ".gdshader"))
return load(path) as Shader
func _init():
# Godot 4 does not have a plain WindowDialog class... there is Window but it's too unfriendly...
get_ok_button().hide()
_progress_window = HT_ProgressWindowScene.instantiate()
add_child(_progress_window)
func _ready():
_inspector.set_prototype({
"seed": {
"type": TYPE_INT,
"randomizable": true,
"range": { "min": -100, "max": 100 },
"slidable": false
},
"offset": {
"type": TYPE_VECTOR2
},
"base_height": {
"type": TYPE_FLOAT,
"range": {"min": -500.0, "max": 500.0, "step": 0.1 },
"default_value": -50.0
},
"height_range": {
"type": TYPE_FLOAT,
"range": {"min": 0.0, "max": 2000.0, "step": 0.1 },
"default_value": 150.0
},
"scale": {
"type": TYPE_FLOAT,
"range": {"min": 1.0, "max": 1000.0, "step": 1.0},
"default_value": 100.0
},
"roughness": {
"type": TYPE_FLOAT,
"range": {"min": 0.0, "max": 1.0, "step": 0.01},
"default_value": 0.4
},
"curve": {
"type": TYPE_FLOAT,
"range": {"min": 1.0, "max": 10.0, "step": 0.1},
"default_value": 1.0
},
"octaves": {
"type": TYPE_INT,
"range": {"min": 1, "max": 10, "step": 1},
"default_value": 6
},
"erosion_steps": {
"type": TYPE_INT,
"range": {"min": 0, "max": 100, "step": 1},
"default_value": 0
},
"erosion_weight": {
"type": TYPE_FLOAT,
"range": { "min": 0.0, "max": 1.0 },
"default_value": 0.5
},
"erosion_slope_factor": {
"type": TYPE_FLOAT,
"range": { "min": 0.0, "max": 1.0 },
"default_value": 0.0
},
"erosion_slope_direction": {
"type": TYPE_VECTOR2,
"default_value": Vector2(0, 0)
},
"erosion_slope_invert": {
"type": TYPE_BOOL,
"default_value": false
},
"dilation": {
"type": TYPE_FLOAT,
"range": { "min": 0.0, "max": 1.0 },
"default_value": 0.0
},
"island_weight": {
"type": TYPE_FLOAT,
"range": { "min": 0.0, "max": 1.0, "step": 0.01 },
"default_value": 0.0
},
"island_sharpness": {
"type": TYPE_FLOAT,
"range": { "min": 0.0, "max": 1.0, "step": 0.01 },
"default_value": 0.0
},
"island_height_ratio": {
"type": TYPE_FLOAT,
"range": { "min": -1.0, "max": 1.0, "step": 0.01 },
"default_value": -1.0
},
"island_shape": {
"type": TYPE_FLOAT,
"range": { "min": 0.0, "max": 1.0, "step": 0.01 },
"default_value": 0.0
},
"additive_heightmap": {
"type": TYPE_BOOL,
"default_value": false
},
"show_sea": {
"type": TYPE_BOOL,
"default_value": true
},
"shadows": {
"type": TYPE_BOOL,
"default_value": true
}
})
_generator = HT_TextureGenerator.new()
_generator.set_resolution(Vector2i(_viewport_resolution, _viewport_resolution))
# Setup the extra pixels we want on max edges for terrain
# TODO I wonder if it's not better to let the generator shaders work in pixels
# instead of NDC, rather than putting a padding system there
_generator.set_output_padding([0, 1, 0, 1])
_generator.output_generated.connect(_on_TextureGenerator_output_generated)
_generator.completed.connect(_on_TextureGenerator_completed)
_generator.progress_reported.connect(_on_TextureGenerator_progress_reported)
add_child(_generator)
# TEST
if not Engine.is_editor_hint():
call_deferred("popup_centered")
func apply_dpi_scale(dpi_scale: float):
min_size *= dpi_scale
_inspector_container.custom_minimum_size *= dpi_scale
func set_terrain(terrain: HTerrain):
_terrain = terrain
_adjust_viewport_resolution()
func _adjust_viewport_resolution():
if _terrain == null:
return
var data = _terrain.get_data()
if data == null:
return
var terrain_resolution := data.get_resolution()
# By default we want to work with a large enough viewport to generate tiles,
# but we should pick a smaller size if the terrain is smaller than that...
var vp_res := MAX_VIEWPORT_RESOLUTION
while vp_res > terrain_resolution:
vp_res /= 2
_generator.set_resolution(Vector2(vp_res, vp_res))
_viewport_resolution = vp_res
func set_image_cache(image_cache: HT_ImageFileCache):
_image_cache = image_cache
func set_undo_redo(ur: EditorUndoRedoManager):
_undo_redo_manager = ur
func _notification(what: int):
match what:
NOTIFICATION_VISIBILITY_CHANGED:
# We don't want any of this to run in an edited scene
if HT_Util.is_in_edited_scene(self):
return
# Since Godot 4 visibility can be changed even between _enter_tree and _ready
if _preview == null:
return
if visible:
# TODO https://github.com/godotengine/godot/issues/18160
if _dialog_visible:
return
_dialog_visible = true
_adjust_viewport_resolution()
_preview.set_sea_visible(_inspector.get_value("show_sea"))
_preview.set_shadows_enabled(_inspector.get_value("shadows"))
_update_generator(true)
else:
# if not _applying:
# _destroy_viewport()
_preview.cleanup()
for i in len(_generated_textures):
_generated_textures[i] = null
_dialog_visible = false
func _update_generator(preview: bool):
var scale : float = _inspector.get_value("scale")
# Scale is inverted in the shader
if absf(scale) < 0.01:
scale = 0.0
else:
scale = 1.0 / scale
scale *= _viewport_resolution
var preview_scale := 4.0 # As if 2049x2049
var sectors := []
var terrain_size := 513
var additive_heightmap : Texture2D = null
# For testing
if not Engine.is_editor_hint() and _terrain == null:
sectors.append(Vector2(0, 0))
# Get preview scale and sectors to generate.
# Allowing null terrain to make it testable.
if _terrain != null and _terrain.get_data() != null:
var terrain_data := _terrain.get_data()
terrain_size = terrain_data.get_resolution()
if _inspector.get_value("additive_heightmap"):
additive_heightmap = terrain_data.get_texture(HTerrainData.CHANNEL_HEIGHT)
if preview:
# When previewing the resolution does not span the entire terrain,
# so we apply a scale to some of the passes to make it cover it all.
preview_scale = float(terrain_size) / float(_viewport_resolution)
sectors.append(Vector2(0, 0))
else:
if additive_heightmap != null:
# We have to duplicate the heightmap because we are going to write
# into it during the generation process.
# It would be fine when we don't read outside of a generated tile,
# but we actually do that for erosion: neighboring pixels are read
# again, and if they were modified by a previous tile it will
# disrupt generation, so we need to use a copy of the original.
additive_heightmap = additive_heightmap.duplicate()
# When we get to generate it fully, sectors are used,
# so the size or shape of the terrain doesn't matter
preview_scale = 1.0
var cw := terrain_size / _viewport_resolution
var ch := terrain_size / _viewport_resolution
for y in ch:
for x in cw:
sectors.append(Vector2(x, y))
var erosion_iterations := int(_inspector.get_value("erosion_steps"))
erosion_iterations /= int(preview_scale)
_generator.clear_passes()
# Terrain textures need to have an off-by-one on their max edge,
# which is shared with the other sectors.
var base_offset_ndc = _inspector.get_value("offset")
#var sector_size_offby1_ndc = float(VIEWPORT_RESOLUTION - 1) / padded_viewport_resolution
for i in len(sectors):
var sector = sectors[i]
#var offset = sector * sector_size_offby1_ndc - Vector2(pad_offset_ndc, pad_offset_ndc)
# var offset_px = sector * (VIEWPORT_RESOLUTION - 1) - Vector2(pad_offset_px, pad_offset_px)
# var offset_ndc = offset_px / padded_viewport_resolution
var progress := float(i) / len(sectors)
var p := HT_TextureGeneratorPass.new()
p.clear = true
p.shader = get_shader("perlin_noise")
# This pass generates the shapes of the terrain so will have to account for offset
p.tile_pos = sector
p.params = {
"u_octaves": _inspector.get_value("octaves"),
"u_seed": _inspector.get_value("seed"),
"u_scale": scale,
"u_offset": base_offset_ndc,
"u_base_height": _inspector.get_value("base_height") / preview_scale,
"u_height_range": _inspector.get_value("height_range") / preview_scale,
"u_roughness": _inspector.get_value("roughness"),
"u_curve": _inspector.get_value("curve"),
"u_island_weight": _inspector.get_value("island_weight"),
"u_island_sharpness": _inspector.get_value("island_sharpness"),
"u_island_height_ratio": _inspector.get_value("island_height_ratio"),
"u_island_shape": _inspector.get_value("island_shape"),
"u_additive_heightmap": additive_heightmap,
"u_additive_heightmap_factor": \
(1.0 if additive_heightmap != null else 0.0) / preview_scale,
"u_terrain_size": terrain_size / preview_scale,
"u_tile_size": _viewport_resolution
}
_generator.add_pass(p)
if erosion_iterations > 0:
p = HT_TextureGeneratorPass.new()
p.shader = get_shader("erode")
# TODO More erosion config
p.params = {
"u_slope_factor": _inspector.get_value("erosion_slope_factor"),
"u_slope_invert": _inspector.get_value("erosion_slope_invert"),
"u_slope_up": _inspector.get_value("erosion_slope_direction"),
"u_weight": _inspector.get_value("erosion_weight"),
"u_dilation": _inspector.get_value("dilation")
}
p.iterations = erosion_iterations
p.padding = p.iterations
_generator.add_pass(p)
_generator.add_output({
"maptype": HTerrainData.CHANNEL_HEIGHT,
"sector": sector,
"progress": progress
})
p = HT_TextureGeneratorPass.new()
p.shader = get_shader("bump2normal")
p.padding = 1
_generator.add_pass(p)
_generator.add_output({
"maptype": HTerrainData.CHANNEL_NORMAL,
"sector": sector,
"progress": progress
})
# TODO AO generation
# TODO Splat generation
_generator.run()
func _on_CancelButton_pressed():
hide()
func _on_ApplyButton_pressed():
# We used to hide the dialog when the Apply button is clicked, and then texture generation took
# place in an offscreen viewport in multiple tiled stages, with a progress window being shown.
# But in Godot 4, it turns out SubViewports never update if they are child of a hidden Window,
# even if they are set to UPDATE_ALWAYS...
#hide()
_apply()
func _on_Inspector_property_changed(key, value):
match key:
"show_sea":
_preview.set_sea_visible(value)
"shadows":
_preview.set_shadows_enabled(value)
_:
_update_generator(true)
func _on_TerrainPreview_dragged(relative: Vector2, button_mask: int):
if button_mask & MOUSE_BUTTON_MASK_LEFT:
var offset : Vector2 = _inspector.get_value("offset")
offset += relative
_inspector.set_value("offset", offset)
func _apply():
if _terrain == null:
_logger.error("cannot apply, terrain is null")
return
var data := _terrain.get_data()
if data == null:
_logger.error("cannot apply, terrain data is null")
return
var dst_heights := data.get_image(HTerrainData.CHANNEL_HEIGHT)
if dst_heights == null:
_logger.error("terrain heightmap image isn't loaded")
return
var dst_normals := data.get_image(HTerrainData.CHANNEL_NORMAL)
if dst_normals == null:
_logger.error("terrain normal image isn't loaded")
return
_applying = true
_undo_map_ids[HTerrainData.CHANNEL_HEIGHT] = _image_cache.save_image(dst_heights)
_undo_map_ids[HTerrainData.CHANNEL_NORMAL] = _image_cache.save_image(dst_normals)
_update_generator(false)
func _on_TextureGenerator_progress_reported(info: Dictionary):
if _applying:
return
var p := 0.0
if info.pass_index == 1:
p = float(info.iteration) / float(info.iteration_count)
_progress_bar.show()
_progress_bar.ratio = p
func _on_TextureGenerator_output_generated(image: Image, info: Dictionary):
# TODO We should check the terrain's image format,
# but that would prevent from testing in isolation...
if info.maptype == HTerrainData.CHANNEL_HEIGHT:
# Hack to workaround Godot 4.0 not supporting RF viewports. Heights are packed as floats
# into RGBA8 components.
assert(image.get_format() == Image.FORMAT_RGBA8)
image = Image.create_from_data(image.get_width(), image.get_height(), false,
Image.FORMAT_RF, image.get_data())
if not _applying:
# Update preview
# TODO Improve TextureGenerator so we can get a ViewportTexture per output?
var tex = _generated_textures[info.maptype]
if tex == null:
tex = ImageTexture.create_from_image(image)
_generated_textures[info.maptype] = tex
else:
tex.update(image)
var num_set := 0
for v in _generated_textures:
if v != null:
num_set += 1
if num_set == len(_generated_textures):
_preview.setup( \
_generated_textures[HTerrainData.CHANNEL_HEIGHT],
_generated_textures[HTerrainData.CHANNEL_NORMAL])
else:
assert(_terrain != null)
var data := _terrain.get_data()
assert(data != null)
var dst := data.get_image(info.maptype)
assert(dst != null)
# print("Tile ", info.sector)
# image.save_png(str("debug_generator_tile_",
# info.sector.x, "_", info.sector.y, "_map", info.maptype, ".png"))
# Converting in case Viewport texture isn't the format we expect for this map.
# Note, in Godot 4 it seems the chosen renderer also influences what you get.
# Forward+ non-transparent viewport gives RGB8, but Compatibility gives RGBA8.
# I don't know if it's expected or is a bug...
# Also, since RF heightmaps we use RGBA8 so we can pack floats in pixels, because
# Godot 4.0 does not support RF viewports. But that also means the same viewport may be
# re-used for other maps that don't need to be RGBA8.
if image.get_format() != dst.get_format():
image.convert(dst.get_format())
dst.blit_rect(image, \
Rect2i(0, 0, image.get_width(), image.get_height()), \
info.sector * _viewport_resolution)
_notify_progress({
"progress": info.progress,
"message": "Calculating sector ("
+ str(info.sector.x) + ", " + str(info.sector.y) + ")"
})
# if info.maptype == HTerrainData.CHANNEL_NORMAL:
# image.save_png(str("normal_sector_", info.sector.x, "_", info.sector.y, ".png"))
func _on_TextureGenerator_completed():
_progress_bar.hide()
if not _applying:
return
_applying = false
assert(_terrain != null)
var data : HTerrainData = _terrain.get_data()
var resolution := data.get_resolution()
data.notify_region_change(Rect2(0, 0, resolution, resolution), HTerrainData.CHANNEL_HEIGHT)
var redo_map_ids := {}
for map_type in _undo_map_ids:
redo_map_ids[map_type] = _image_cache.save_image(data.get_image(map_type))
var undo_redo := _undo_redo_manager.get_history_undo_redo(
_undo_redo_manager.get_object_history_id(data))
data._edit_set_disable_apply_undo(true)
undo_redo.create_action("Generate terrain")
undo_redo.add_do_method(data._edit_apply_maps_from_file_cache.bind(_image_cache, redo_map_ids))
undo_redo.add_undo_method(
data._edit_apply_maps_from_file_cache.bind(_image_cache, _undo_map_ids))
undo_redo.commit_action()
data._edit_set_disable_apply_undo(false)
_notify_progress({ "finished": true })
_logger.debug("Done")
hide()
func _notify_progress(info: Dictionary):
_progress_window.handle_progress(info)
func _process(delta):
if _applying:
# HACK to workaround a peculiar behavior of Viewports in Godot 4.
# Apparently Godot 4 will not update Viewports set to UPDATE_ALWAYS when the editor decides
# it doesn't need to redraw ("low processor mode", what makes the editor redraw only with
# changes). That wasn't the case in Godot 3, but I guess it is now.
# That means when we click Apply, the viewport will not update in particular when doing
# erosion passes, because the action of clicking Apply doesn't lead to as many redraws as
# changing preview parameters in the UI (those cause redraws for different reasons).
# So let's poke the renderer by redrawing something...
#
# This also piles on top of the workaround in which we keep the window visible when
# applying! So the window has one more reason to stay visible...
#
_preview.queue_redraw()

View File

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

View File

@@ -0,0 +1,84 @@
[gd_scene load_steps=5 format=3 uid="uid://cgfo1ocbdi1ug"]
[ext_resource type="Script" uid="uid://dnfubafis3xck" path="res://addons/zylann.hterrain/tools/generator/generator_dialog.gd" id="1"]
[ext_resource type="PackedScene" uid="uid://dfjip6c4olemn" path="res://addons/zylann.hterrain/tools/inspector/inspector.tscn" id="2"]
[ext_resource type="PackedScene" uid="uid://bue2flijnxa3p" path="res://addons/zylann.hterrain/tools/terrain_preview.tscn" id="3"]
[ext_resource type="PackedScene" path="res://addons/zylann.hterrain/tools/util/dialog_fitter.tscn" id="4"]
[node name="GeneratorDialog" type="AcceptDialog"]
title = "Generate terrain"
size = Vector2i(1100, 780)
min_size = Vector2i(1100, 620)
script = ExtResource("1")
[node name="VBoxContainer" 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 = -18.0
[node name="Editor" type="HBoxContainer" parent="VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3
[node name="Settings" type="VBoxContainer" parent="VBoxContainer/Editor"]
custom_minimum_size = Vector2(420, 0)
layout_mode = 2
[node name="Inspector" parent="VBoxContainer/Editor/Settings" instance=ExtResource("2")]
layout_mode = 2
[node name="Preview" type="Control" parent="VBoxContainer/Editor"]
layout_mode = 2
size_flags_horizontal = 3
[node name="TerrainPreview" parent="VBoxContainer/Editor/Preview" instance=ExtResource("3")]
layout_mode = 1
anchors_preset = 15
grow_horizontal = 2
grow_vertical = 2
[node name="Label" type="Label" parent="VBoxContainer/Editor/Preview"]
layout_mode = 0
offset_left = 5.0
offset_top = 4.0
offset_right = 207.0
offset_bottom = 18.0
text = "LMB: offset, MMB: rotate"
[node name="ProgressBar" type="ProgressBar" parent="VBoxContainer/Editor/Preview"]
layout_mode = 1
anchors_preset = -1
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_top = -35.0
step = 1.0
[node name="Choices" type="HBoxContainer" parent="VBoxContainer"]
layout_mode = 2
alignment = 1
[node name="ApplyButton" type="Button" parent="VBoxContainer/Choices"]
layout_mode = 2
text = "Apply"
[node name="CancelButton" type="Button" parent="VBoxContainer/Choices"]
layout_mode = 2
text = "Cancel"
[node name="DialogFitter" parent="." instance=ExtResource("4")]
layout_mode = 3
anchors_preset = 0
offset_left = 8.0
offset_top = 8.0
offset_right = 1092.0
offset_bottom = 762.0
[connection signal="property_changed" from="VBoxContainer/Editor/Settings/Inspector" to="." method="_on_Inspector_property_changed"]
[connection signal="dragged" from="VBoxContainer/Editor/Preview/TerrainPreview" to="." method="_on_TerrainPreview_dragged"]
[connection signal="pressed" from="VBoxContainer/Choices/ApplyButton" to="." method="_on_ApplyButton_pressed"]
[connection signal="pressed" from="VBoxContainer/Choices/CancelButton" to="." method="_on_CancelButton_pressed"]

View File

@@ -0,0 +1,27 @@
shader_type canvas_item;
render_mode blend_disabled;
#include "res://addons/zylann.hterrain/shaders/include/heightmap.gdshaderinc"
//uniform sampler2D u_screen_texture : hint_screen_texture;
uniform sampler2D u_previous_pass;
vec4 pack_normal(vec3 n) {
return vec4((0.5 * (n + 1.0)).xzy, 1.0);
}
float get_height(sampler2D tex, vec2 uv) {
return sample_height_from_viewport(tex, uv);
}
void fragment() {
vec2 uv = SCREEN_UV;
vec2 ps = SCREEN_PIXEL_SIZE;
float left = get_height(u_previous_pass, uv + vec2(-ps.x, 0));
float right = get_height(u_previous_pass, uv + vec2(ps.x, 0));
float back = get_height(u_previous_pass, uv + vec2(0, -ps.y));
float fore = get_height(u_previous_pass, uv + vec2(0, ps.y));
vec3 n = normalize(vec3(left - right, 2.0, fore - back));
COLOR = pack_normal(n);
}

View File

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

View File

@@ -0,0 +1,76 @@
shader_type canvas_item;
render_mode blend_disabled;
#include "res://addons/zylann.hterrain/shaders/include/heightmap.gdshaderinc"
uniform vec2 u_slope_up = vec2(0, 0);
uniform float u_slope_factor = 1.0;
uniform bool u_slope_invert = false;
uniform float u_weight = 0.5;
uniform float u_dilation = 0.0;
//uniform sampler2D u_screen_texture : hint_screen_texture;
uniform sampler2D u_previous_pass;
float get_height(sampler2D tex, vec2 uv) {
return sample_height_from_viewport(tex, uv);
}
void fragment() {
float r = 3.0;
// Divide so the shader stays neighbor dependent 1 pixel across.
// For this to work, filtering must be enabled.
vec2 eps = SCREEN_PIXEL_SIZE / (0.99 * r);
vec2 uv = SCREEN_UV;
float h = get_height(u_previous_pass, 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(float(x), float(y));
float nh = get_height(u_previous_pass, 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, u_weight);
dh = mix(h, dh, u_weight);
float ph = mix(eh, dh, u_dilation);
if (u_slope_factor > 0.0) {
vec2 ps = SCREEN_PIXEL_SIZE;
float left = get_height(u_previous_pass, uv + vec2(-ps.x, 0.0));
float right = get_height(u_previous_pass, uv + vec2(ps.x, 0.0));
float top = get_height(u_previous_pass, uv + vec2(0.0, ps.y));
float bottom = get_height(u_previous_pass, uv + vec2(0.0, -ps.y));
vec3 normal = normalize(vec3(left - right, ps.x + ps.y, bottom - top));
vec3 up = normalize(vec3(u_slope_up.x, 1.0, u_slope_up.y));
float f = max(dot(normal, up), 0);
if (u_slope_invert) {
f = 1.0 - f;
}
ph = mix(h, ph, mix(1.0, f, u_slope_factor));
//COLOR = vec4(f, f, f, 1.0);
}
//COLOR = vec4(0.5 * normal + 0.5, 1.0);
//eh = 0.5 * (eh + texture(SCREEN_TEXTURE, uv + mp * ps * k).r);
//eh = mix(h, eh, (1.0 - h) / r);
COLOR = encode_height_to_viewport(ph);
}

View File

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

View File

@@ -0,0 +1,211 @@
shader_type canvas_item;
// Required only because we use all 4 channels to encode floats into RGBA8
render_mode blend_disabled;
#include "res://addons/zylann.hterrain/shaders/include/heightmap.gdshaderinc"
uniform vec2 u_offset;
uniform float u_scale = 0.02;
uniform float u_base_height = 0.0;
uniform float u_height_range = 100.0;
uniform int u_seed;
uniform int u_octaves = 5;
uniform float u_roughness = 0.5;
uniform float u_curve = 1.0;
uniform float u_terrain_size = 513.0;
uniform float u_tile_size = 513.0;
uniform sampler2D u_additive_heightmap;
uniform float u_additive_heightmap_factor = 0.0;
uniform vec2 u_uv_offset;
uniform vec2 u_uv_scale = vec2(1.0, 1.0);
uniform float u_island_weight = 0.0;
// 0: smooth transition, 1: sharp transition
uniform float u_island_sharpness = 0.0;
// 0: edge is min height (island), 1: edge is max height (canyon)
uniform float u_island_height_ratio = 0.0;
// 0: round, 1: square
uniform float u_island_shape = 0.0;
////////////////////////////////////////////////////////////////////////////////
// Perlin noise source:
// https://github.com/curly-brace/Godot-3.0-Noise-Shaders
//
// GLSL textureless classic 2D noise \"cnoise\",
// with an RSL-style periodic variant \"pnoise\".
// Author: Stefan Gustavson (stefan.gustavson@liu.se)
// Version: 2011-08-22
//
// Many thanks to Ian McEwan of Ashima Arts for the
// ideas for permutation and gradient selection.
//
// Copyright (c) 2011 Stefan Gustavson. All rights reserved.
// Distributed under the MIT license. See LICENSE file.
// https://github.com/stegu/webgl-noise
//
vec4 mod289(vec4 x) {
return x - floor(x * (1.0 / 289.0)) * 289.0;
}
vec4 permute(vec4 x) {
return mod289(((x * 34.0) + 1.0) * x);
}
vec4 taylorInvSqrt(vec4 r) {
return 1.79284291400159 - 0.85373472095314 * r;
}
vec2 fade(vec2 t) {
return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);
}
// Classic Perlin noise
float cnoise(vec2 P) {
vec4 Pi = floor(vec4(P, P)) + vec4(0.0, 0.0, 1.0, 1.0);
vec4 Pf = fract(vec4(P, P)) - vec4(0.0, 0.0, 1.0, 1.0);
Pi = mod289(Pi); // To avoid truncation effects in permutation
vec4 ix = Pi.xzxz;
vec4 iy = Pi.yyww;
vec4 fx = Pf.xzxz;
vec4 fy = Pf.yyww;
vec4 i = permute(permute(ix) + iy);
vec4 gx = fract(i * (1.0 / 41.0)) * 2.0 - 1.0 ;
vec4 gy = abs(gx) - 0.5 ;
vec4 tx = floor(gx + 0.5);
gx = gx - tx;
vec2 g00 = vec2(gx.x,gy.x);
vec2 g10 = vec2(gx.y,gy.y);
vec2 g01 = vec2(gx.z,gy.z);
vec2 g11 = vec2(gx.w,gy.w);
vec4 norm = taylorInvSqrt(vec4(dot(g00, g00), dot(g01, g01), dot(g10, g10), dot(g11, g11)));
g00 *= norm.x;
g01 *= norm.y;
g10 *= norm.z;
g11 *= norm.w;
float n00 = dot(g00, vec2(fx.x, fy.x));
float n10 = dot(g10, vec2(fx.y, fy.y));
float n01 = dot(g01, vec2(fx.z, fy.z));
float n11 = dot(g11, vec2(fx.w, fy.w));
vec2 fade_xy = fade(Pf.xy);
vec2 n_x = mix(vec2(n00, n01), vec2(n10, n11), fade_xy.x);
float n_xy = mix(n_x.x, n_x.y, fade_xy.y);
return 2.3 * n_xy;
}
////////////////////////////////////////////////////////////////////////////////
float get_fractal_noise(vec2 uv) {
float scale = 1.0;
float sum = 0.0;
float amp = 0.0;
int octaves = u_octaves;
float p = 1.0;
uv.x += float(u_seed) * 61.0;
for (int i = 0; i < octaves; ++i) {
sum += p * cnoise(uv * scale);
amp += p;
scale *= 2.0;
p *= u_roughness;
}
float gs = sum / amp;
return gs;
}
// x is a ratio in 0..1
float get_island_curve(float x) {
return smoothstep(min(0.999, u_island_sharpness), 1.0, x);
// float exponent = 1.0 + 10.0 * u_island_sharpness;
// return pow(abs(x), exponent);
}
float smooth_union(float a, float b, float k) {
float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
return mix(b, a, h) - k * h * (1.0 - h);
}
float squareish_distance(vec2 a, vec2 b, float r, float s) {
vec2 v = b - a;
// TODO This is brute force but this is the first attempt that gave me a "rounded square" distance,
// where the "roundings" remained constant over distance (not the case with standard box SDF)
float da = -smooth_union(v.x+s, v.y+s, r)+s;
float db = -smooth_union(s-v.x, s-v.y, r)+s;
float dc = -smooth_union(s-v.x, v.y+s, r)+s;
float dd = -smooth_union(v.x+s, s-v.y, r)+s;
return max(max(da, db), max(dc, dd));
}
// This is too sharp
//float squareish_distance(vec2 a, vec2 b) {
// vec2 v = b - a;
// // Manhattan distance would produce a "diamond-shaped distance".
// // This gives "square-shaped" distance.
// return max(abs(v.x), abs(v.y));
//}
float get_island_distance(vec2 pos, vec2 center, float terrain_size) {
float rd = distance(pos, center);
float sd = squareish_distance(pos, center, terrain_size * 0.1, terrain_size);
return mix(rd, sd, u_island_shape);
}
// pos is in terrain space
float get_height(vec2 pos) {
float h = 0.0;
{
// Noise (0..1)
// Offset and scale for the noise itself
vec2 uv_noise = (pos / u_terrain_size + u_offset) * u_scale;
h = 0.5 + 0.5 * get_fractal_noise(uv_noise);
}
// Curve
{
h = pow(h, u_curve);
}
// Island
{
float terrain_size = u_terrain_size;
vec2 island_center = vec2(0.5 * terrain_size);
float island_height_ratio = 0.5 + 0.5 * u_island_height_ratio;
float island_distance = get_island_distance(pos, island_center, terrain_size);
float distance_ratio = clamp(island_distance / (0.5 * terrain_size), 0.0, 1.0);
float island_ratio = u_island_weight * get_island_curve(distance_ratio);
h = mix(h, island_height_ratio, island_ratio);
}
// Height remapping
{
h = u_base_height + h * u_height_range;
}
// Additive heightmap
{
vec2 uv = pos / u_terrain_size;
float ah = sample_heightmap(u_additive_heightmap, uv);
h += u_additive_heightmap_factor * ah;
}
return h;
}
void fragment() {
// Handle screen padding: transform UV back into generation space.
// This is in tile space actually...? it spans 1 unit across the viewport,
// and starts from 0 when tile (0,0) is generated.
// Maybe we could change this into world units instead?
vec2 uv_tile = (SCREEN_UV + u_uv_offset) * u_uv_scale;
float h = get_height(uv_tile * u_tile_size);
COLOR = encode_height_to_viewport(h);
}

View File

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

View File

@@ -0,0 +1,331 @@
# Holds a viewport on which several passes may run to generate a final image.
# Passes can have different shaders and re-use what was drawn by a previous pass.
# TODO I'd like to make such a system working as a graph of passes for more possibilities.
@tool
extends Node
const HT_Util = preload("res://addons/zylann.hterrain/util/util.gd")
const HT_TextureGeneratorPass = preload("./texture_generator_pass.gd")
const HT_Logger = preload("../../util/logger.gd")
# TODO Can't preload because it causes the plugin to fail loading if assets aren't imported
const DUMMY_TEXTURE_PATH = "res://addons/zylann.hterrain/tools/icons/empty.png"
signal progress_reported(info)
# Emitted when an output is generated.
signal output_generated(image, metadata)
# Emitted when all passes are complete
signal completed
class HT_TextureGeneratorViewport:
var viewport : SubViewport
var ci : TextureRect
var _passes := []
var _resolution := Vector2i(512, 512)
var _output_padding := [0, 0, 0, 0]
# Since Godot 4.0, we use ping-pong viewports because `hint_screen_texture` always returns `1.0`
# for transparent pixels, which is wrong, but sadly appears to be intented...
# https://github.com/godotengine/godot/issues/78207
var _viewports : Array[HT_TextureGeneratorViewport] = [null, null]
var _viewport_index := 0
var _dummy_texture : Texture2D
var _running := false
var _rerun := false
#var _tiles = PoolVector2Array([Vector2()])
var _running_passes := []
var _running_pass_index := 0
var _running_iteration := 0
var _shader_material : ShaderMaterial = null
#var _uv_offset = 0 # Offset de to padding
var _logger = HT_Logger.get_for(self)
func _ready():
_dummy_texture = load(DUMMY_TEXTURE_PATH)
if _dummy_texture == null:
_logger.error(str("Failed to load dummy texture ", DUMMY_TEXTURE_PATH))
for viewport_index in len(_viewports):
var viewport = SubViewport.new()
# We render with 2D shaders, but we don't want the parent world to interfere
viewport.own_world_3d = true
viewport.world_3d = World3D.new()
viewport.render_target_update_mode = SubViewport.UPDATE_DISABLED
# Require RGBA8 so we can pack heightmap floats into pixels
viewport.transparent_bg = true
add_child(viewport)
var ci := TextureRect.new()
ci.stretch_mode = TextureRect.STRETCH_SCALE
ci.texture = _dummy_texture
viewport.add_child(ci)
var vp := HT_TextureGeneratorViewport.new()
vp.viewport = viewport
vp.ci = ci
_viewports[viewport_index] = vp
_shader_material = ShaderMaterial.new()
set_process(false)
func is_running() -> bool:
return _running
func clear_passes():
_passes.clear()
func add_pass(p: HT_TextureGeneratorPass):
assert(_passes.find(p) == -1)
assert(p.iterations > 0)
_passes.append(p)
func add_output(meta):
assert(len(_passes) > 0)
var p = _passes[-1]
p.output = true
p.metadata = meta
# Sets at which base resolution the generator will work on.
# In tiled rendering, this is the resolution of one tile.
# The internal viewport may be larger if some passes need more room,
# and the resulting images might include some of these pixels if output padding is used.
func set_resolution(res: Vector2i):
assert(not _running)
_resolution = res
# Tell image outputs to include extra pixels on the edges.
# This extends the resolution of images compared to the base resolution.
# The initial use case for this is to generate terrain tiles where edge pixels are
# shared with the neighor tiles.
func set_output_padding(p: Array):
assert(typeof(p) == TYPE_ARRAY)
assert(len(p) == 4)
for v in p:
assert(typeof(v) == TYPE_INT)
_output_padding = p
func run():
assert(len(_passes) > 0)
if _running:
_rerun = true
return
for vp in _viewports:
assert(vp.viewport != null)
assert(vp.ci != null)
# Copy passes
var passes := []
passes.resize(len(_passes))
for i in len(_passes):
passes[i] = _passes[i].duplicate()
_running_passes = passes
# Pad pixels according to largest padding
var largest_padding := 0
for p in passes:
if p.padding > largest_padding:
largest_padding = p.padding
for v in _output_padding:
if v > largest_padding:
largest_padding = v
var padded_size := _resolution + 2 * Vector2i(largest_padding, largest_padding)
# _uv_offset = Vector2( \
# float(largest_padding) / padded_size.x,
# float(largest_padding) / padded_size.y)
for vp in _viewports:
vp.ci.size = padded_size
vp.viewport.size = padded_size
# First viewport index doesn't matter.
# Maybe one issue of resetting it to zero would be that the previous run
# could have ended with the same viewport that will be sent as a texture as
# one of the uniforms of the shader material, which causes an error in the
# renderer because it's not allowed to use a viewport texture while
# rendering on the same viewport
# _viewport_index = 0
var first_vp := _viewports[_viewport_index]
first_vp.viewport.render_target_clear_mode = SubViewport.CLEAR_MODE_ONCE
# I don't trust `UPDATE_ONCE`, it also doesn't reset so we never know if it actually works...
# https://github.com/godotengine/godot/issues/33351
first_vp.viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
for vp in _viewports:
if vp != first_vp:
vp.viewport.render_target_update_mode = SubViewport.UPDATE_DISABLED
_running_pass_index = 0
_running_iteration = 0
_running = true
set_process(true)
func _process(delta: float):
# TODO because of https://github.com/godotengine/godot/issues/7894
if not is_processing():
return
var prev_vpi := 0 if _viewport_index == 1 else 1
var prev_vp := _viewports[prev_vpi]
if _running_pass_index > 0:
var prev_pass : HT_TextureGeneratorPass = _running_passes[_running_pass_index - 1]
if prev_pass.output:
_create_output_image(prev_pass.metadata, prev_vp)
if _running_pass_index >= len(_running_passes):
_running = false
completed.emit()
if _rerun:
# run() was requested again before we complete...
# this will happen very frequently because we are forced to wait multiple frames
# before getting a result
_rerun = false
run()
else:
# Done
for vp in _viewports:
vp.viewport.render_target_update_mode = SubViewport.UPDATE_DISABLED
set_process(false)
return
var p : HT_TextureGeneratorPass = _running_passes[_running_pass_index]
var vp := _viewports[_viewport_index]
vp.viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
prev_vp.viewport.render_target_update_mode = SubViewport.UPDATE_DISABLED
if _running_iteration == 0:
_setup_pass(p, vp)
_setup_iteration(vp, prev_vp)
_report_progress(_running_passes, _running_pass_index, _running_iteration)
# Wait one frame for render, and this for EVERY iteration and every pass,
# because Godot doesn't provide any way to run multiple feedback render passes in one go.
_running_iteration += 1
if _running_iteration == p.iterations:
_running_iteration = 0
_running_pass_index += 1
# Swap viewport for next pass
_viewport_index = (_viewport_index + 1) % 2
# The viewport should render after the tree was processed
# Called at the beginning of each pass
func _setup_pass(p: HT_TextureGeneratorPass, vp: HT_TextureGeneratorViewport):
if p.texture != null:
vp.ci.texture = p.texture
else:
vp.ci.texture = _dummy_texture
if p.shader != null:
if _shader_material == null:
_shader_material = ShaderMaterial.new()
_shader_material.shader = p.shader
vp.ci.material = _shader_material
if p.params != null:
for param_name in p.params:
_shader_material.set_shader_parameter(param_name, p.params[param_name])
var vp_size_f := Vector2(vp.viewport.size)
var res_f := Vector2(_resolution)
var scale_ndc := vp_size_f / res_f
var pad_offset_ndc := ((vp_size_f - res_f) / 2.0) / vp_size_f
var offset_ndc := -pad_offset_ndc + p.tile_pos / scale_ndc
# Because padding may be used around the generated area,
# the shader can use these predefined parameters,
# and apply the following to SCREEN_UV to adjust its calculations:
# vec2 uv = (SCREEN_UV + u_uv_offset) * u_uv_scale;
if p.params == null or not p.params.has("u_uv_scale"):
_shader_material.set_shader_parameter("u_uv_scale", scale_ndc)
if p.params == null or not p.params.has("u_uv_offset"):
_shader_material.set_shader_parameter("u_uv_offset", offset_ndc)
else:
vp.ci.material = null
if p.clear:
vp.viewport.render_target_clear_mode = SubViewport.CLEAR_MODE_ONCE
# Called for every iteration of every pass
func _setup_iteration(vp: HT_TextureGeneratorViewport, prev_vp: HT_TextureGeneratorViewport):
assert(vp != prev_vp)
if _shader_material != null:
_shader_material.set_shader_parameter("u_previous_pass", prev_vp.viewport.get_texture())
func _create_output_image(metadata, vp: HT_TextureGeneratorViewport):
var tex := vp.viewport.get_texture()
var src := tex.get_image()
# src.save_png(str("ddd_tgen_output", metadata.maptype, ".png"))
# Pick the center of the image
var subrect := Rect2i( \
(src.get_width() - _resolution.x) / 2, \
(src.get_height() - _resolution.y) / 2, \
_resolution.x, _resolution.y)
# Make sure we are pixel-perfect. If not, padding is odd
# assert(int(subrect.position.x) == subrect.position.x)
# assert(int(subrect.position.y) == subrect.position.y)
subrect.position.x -= _output_padding[0]
subrect.position.y -= _output_padding[2]
subrect.size.x += _output_padding[0] + _output_padding[1]
subrect.size.y += _output_padding[2] + _output_padding[3]
var dst : Image
if subrect == Rect2i(0, 0, src.get_width(), src.get_height()):
dst = src
else:
# Note: size MUST match at this point.
# If it doesn't, the viewport has not been configured properly,
# or padding has been modified while the generator was running
dst = Image.create( \
_resolution.x + _output_padding[0] + _output_padding[1], \
_resolution.y + _output_padding[2] + _output_padding[3], \
false, src.get_format())
dst.blit_rect(src, subrect, Vector2i())
output_generated.emit(dst, metadata)
func _report_progress(passes: Array, pass_index: int, iteration: int):
var p = passes[pass_index]
progress_reported.emit({
"name": p.debug_name,
"pass_index": pass_index,
"pass_count": len(passes),
"iteration": iteration,
"iteration_count": p.iterations
})

View File

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

View File

@@ -0,0 +1,43 @@
# Name of the pass, for debug purposes
var debug_name := ""
# The viewport will be cleared at this pass
var clear := false
# Which main texture should be drawn.
# If not set, a default texture will be drawn.
# Note that it won't matter if the shader disregards it,
# and will only serve to provide UVs, due to https://github.com/godotengine/godot/issues/7298.
var texture : Texture = null
# Which shader to use
var shader : Shader = null
# Parameters for the shader
# TODO Use explicit Dictionary, dont allow null
var params = null
# How many pixels to pad the viewport on all edges, in case neighboring matters.
# Outputs won't have that padding, but can pick part of it in case output padding is used.
var padding := 0
# How many times this pass must be run
var iterations := 1
# If not empty, the viewport will be downloaded as an image before the next pass
var output := false
# Sent along the output
var metadata = null
# Used for tiled rendering, where each tile has the base resolution,
# in case the viewport cannot be made big enough to cover the final image,
# of if you are generating a pseudo-infinite terrain.
# TODO Have an API for this?
var tile_pos := Vector2()
func duplicate():
var p = get_script().new()
p.debug_name = debug_name
p.clear = clear
p.texture = texture
p.shader = shader
p.params = params
p.padding = padding
p.iterations = iterations
p.output = output
p.metadata = metadata
p.tile_pos = tile_pos
return p

View File

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

View File

@@ -0,0 +1,168 @@
# Bakes a global albedo map using the same shader the terrain uses,
# but renders top-down in orthographic mode.
@tool
extends Node
const HTerrain = preload("../hterrain.gd")
const HTerrainData = preload("../hterrain_data.gd")
const HTerrainMesher = preload("../hterrain_mesher.gd")
# Must be power of two
const DEFAULT_VIEWPORT_SIZE = 512
signal progress_notified(info)
signal permanent_change_performed(message)
var _terrain : HTerrain = null
var _viewport : SubViewport = null
var _viewport_size := DEFAULT_VIEWPORT_SIZE
var _plane : MeshInstance3D = null
var _camera : Camera3D = null
var _sectors := []
var _sector_index := 0
func _ready():
set_process(false)
func bake(terrain: HTerrain):
assert(terrain != null)
var data := terrain.get_data()
assert(data != null)
_terrain = terrain
var splatmap := data.get_texture(HTerrainData.CHANNEL_SPLAT)
var colormap := data.get_texture(HTerrainData.CHANNEL_COLOR)
var terrain_size := data.get_resolution()
if _viewport == null:
_setup_scene(terrain_size)
var cw := terrain_size / _viewport_size
var ch := terrain_size / _viewport_size
for y in ch:
for x in cw:
_sectors.append(Vector2(x, y))
var mat := _plane.material_override
_terrain.setup_globalmap_material(mat)
_sector_index = 0
set_process(true)
func _setup_scene(terrain_size: int):
assert(_viewport == null)
_viewport_size = DEFAULT_VIEWPORT_SIZE
while _viewport_size > terrain_size:
_viewport_size /= 2
_viewport = SubViewport.new()
_viewport.size = Vector2(_viewport_size + 1, _viewport_size + 1)
_viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
_viewport.render_target_clear_mode = SubViewport.CLEAR_MODE_ALWAYS
# _viewport.render_target_v_flip = true
_viewport.world_3d = World3D.new()
_viewport.own_world_3d = true
_viewport.debug_draw = Viewport.DEBUG_DRAW_UNSHADED
var mat := ShaderMaterial.new()
_plane = MeshInstance3D.new()
# Make a very small mesh, vertex precision isn't required
var plane_res := 4
_plane.mesh = \
HTerrainMesher.make_flat_chunk(plane_res, plane_res, _viewport_size / plane_res, 0)
_plane.material_override = mat
_viewport.add_child(_plane)
_camera = Camera3D.new()
_camera.projection = Camera3D.PROJECTION_ORTHOGONAL
_camera.size = _viewport.size.x
_camera.near = 0.1
_camera.far = 10.0
_camera.current = true
_camera.rotation = Vector3(deg_to_rad(-90), 0, 0)
_viewport.add_child(_camera)
add_child(_viewport)
func _cleanup_scene():
_viewport.queue_free()
_viewport = null
_plane = null
_camera = null
func _process(delta):
if not is_processing():
return
if _sector_index > 0:
_grab_image(_sectors[_sector_index - 1])
if _sector_index >= len(_sectors):
set_process(false)
_finish()
progress_notified.emit({ "finished": true })
else:
_setup_pass(_sectors[_sector_index])
_report_progress()
_sector_index += 1
func _report_progress():
var sector = _sectors[_sector_index]
progress_notified.emit({
"progress": float(_sector_index) / len(_sectors),
"message": "Calculating sector (" + str(sector.x) + ", " + str(sector.y) + ")"
})
func _setup_pass(sector: Vector2):
# Note: we implicitely take off-by-one pixels into account
var origin := sector * _viewport_size
var center := origin + 0.5 * Vector2(_viewport.size)
# The heightmap is left empty, so will default to white, which is a height of 1.
# The camera must be placed above the terrain to see it.
_camera.position = Vector3(center.x, 2.0, center.y)
_plane.position = Vector3(origin.x, 0.0, origin.y)
func _grab_image(sector: Vector2):
var tex := _viewport.get_texture()
var src := tex.get_image()
assert(_terrain != null)
var data := _terrain.get_data()
assert(data != null)
if data.get_map_count(HTerrainData.CHANNEL_GLOBAL_ALBEDO) == 0:
data._edit_add_map(HTerrainData.CHANNEL_GLOBAL_ALBEDO)
var dst := data.get_image(HTerrainData.CHANNEL_GLOBAL_ALBEDO)
src.convert(dst.get_format())
var origin = sector * _viewport_size
dst.blit_rect(src, Rect2i(0, 0, src.get_width(), src.get_height()), origin)
func _finish():
assert(_terrain != null)
var data := _terrain.get_data() as HTerrainData
assert(data != null)
var dst := data.get_image(HTerrainData.CHANNEL_GLOBAL_ALBEDO)
data.notify_region_change(Rect2(0, 0, dst.get_width(), dst.get_height()),
HTerrainData.CHANNEL_GLOBAL_ALBEDO)
permanent_change_performed.emit("Bake globalmap")
_cleanup_scene()
_terrain = null

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 832 B

View File

@@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://vo2brkj3udel"
path="res://.godot/imported/empty.png-31363f083c9c4e2e8e54cf64f3716737.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/zylann.hterrain/tools/icons/empty.png"
dest_files=["res://.godot/imported/empty.png-31363f083c9c4e2e8e54cf64f3716737.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,66 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="16"
height="16"
version="1.1"
viewBox="0 0 16 16"
id="svg2"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
sodipodi:docname="icon_anchor_bottom.svg">
<metadata
id="metadata12">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs10" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1461"
inkscape:window-height="826"
id="namedview8"
showgrid="true"
inkscape:snap-grids="true"
inkscape:snap-global="true"
inkscape:snap-bbox="false"
inkscape:snap-page="true"
inkscape:object-nodes="true"
inkscape:zoom="32"
inkscape:cx="7.7923218"
inkscape:cy="7.6837119"
inkscape:window-x="156"
inkscape:window-y="100"
inkscape:window-maximized="0"
inkscape:current-layer="svg2">
<inkscape:grid
type="xygrid"
id="grid4142" />
</sodipodi:namedview>
<path
style="fill:#e0e0e0;fill-opacity:1;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 8,14 4,8 H 6 V 2 h 4 v 6 h 2 z"
id="path816"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccc" />
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://l16qrsohg7jj"
path="res://.godot/imported/icon_anchor_bottom.svg-963f115d31a41c38349ab03453cf2ef5.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/zylann.hterrain/tools/icons/icon_anchor_bottom.svg"
dest_files=["res://.godot/imported/icon_anchor_bottom.svg-963f115d31a41c38349ab03453cf2ef5.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
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="16"
height="16"
version="1.1"
viewBox="0 0 16 16"
id="svg2"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
sodipodi:docname="icon_anchor_bottom_left.svg">
<metadata
id="metadata12">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs10" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1461"
inkscape:window-height="826"
id="namedview8"
showgrid="true"
inkscape:snap-grids="true"
inkscape:snap-global="true"
inkscape:snap-bbox="false"
inkscape:snap-page="true"
inkscape:object-nodes="true"
inkscape:zoom="32"
inkscape:cx="7.7923218"
inkscape:cy="7.6837119"
inkscape:window-x="156"
inkscape:window-y="100"
inkscape:window-maximized="0"
inkscape:current-layer="svg2">
<inkscape:grid
type="xygrid"
id="grid4142" />
</sodipodi:namedview>
<path
style="fill:#e0e0e0;fill-opacity:1;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 3.0307757,12.969224 1.414214,-7.071068 1.414213,1.4142142 4.2426413,-4.2426412 2.828427,2.828427 -4.2426407,4.242641 1.4142137,1.414214 z"
id="path816"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccc" />
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://30wsjmeemngg"
path="res://.godot/imported/icon_anchor_bottom_left.svg-c59f20ff71f725e47b5fc556b5ef93c4.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/zylann.hterrain/tools/icons/icon_anchor_bottom_left.svg"
dest_files=["res://.godot/imported/icon_anchor_bottom_left.svg-c59f20ff71f725e47b5fc556b5ef93c4.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
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="16"
height="16"
version="1.1"
viewBox="0 0 16 16"
id="svg2"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
sodipodi:docname="icon_anchor_bottom_right.svg">
<metadata
id="metadata12">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs10" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1461"
inkscape:window-height="826"
id="namedview8"
showgrid="true"
inkscape:snap-grids="true"
inkscape:snap-global="true"
inkscape:snap-bbox="false"
inkscape:snap-page="true"
inkscape:object-nodes="true"
inkscape:zoom="32"
inkscape:cx="7.7923218"
inkscape:cy="7.6837119"
inkscape:window-x="156"
inkscape:window-y="100"
inkscape:window-maximized="0"
inkscape:current-layer="svg2">
<inkscape:grid
type="xygrid"
id="grid4142" />
</sodipodi:namedview>
<path
style="fill:#e0e0e0;fill-opacity:1;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 13,12.969224 5.9289322,11.55501 7.3431464,10.140797 3.1005052,5.8981558 5.9289322,3.0697288 10.171573,7.3123695 11.585787,5.8981558 Z"
id="path816"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccc" />
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cwb02kd3gr5v1"
path="res://.godot/imported/icon_anchor_bottom_right.svg-23dd5f1d1c7021fe105f8bde603dcc4d.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/zylann.hterrain/tools/icons/icon_anchor_bottom_right.svg"
dest_files=["res://.godot/imported/icon_anchor_bottom_right.svg-23dd5f1d1c7021fe105f8bde603dcc4d.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
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

Some files were not shown because too many files have changed in this diff Show More