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