diff options
Diffstat (limited to 'game/addons/zylann.hterrain/tools/generator')
7 files changed, 1334 insertions, 0 deletions
diff --git a/game/addons/zylann.hterrain/tools/generator/generator_dialog.gd b/game/addons/zylann.hterrain/tools/generator/generator_dialog.gd new file mode 100644 index 0000000..bbb7069 --- /dev/null +++ b/game/addons/zylann.hterrain/tools/generator/generator_dialog.gd @@ -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() + diff --git a/game/addons/zylann.hterrain/tools/generator/generator_dialog.tscn b/game/addons/zylann.hterrain/tools/generator/generator_dialog.tscn new file mode 100644 index 0000000..5d911e3 --- /dev/null +++ b/game/addons/zylann.hterrain/tools/generator/generator_dialog.tscn @@ -0,0 +1,84 @@ +[gd_scene load_steps=5 format=3 uid="uid://cgfo1ocbdi1ug"] + +[ext_resource type="Script" path="res://addons/zylann.hterrain/tools/generator/generator_dialog.gd" id="1"] +[ext_resource type="PackedScene" path="res://addons/zylann.hterrain/tools/inspector/inspector.tscn" id="2"] +[ext_resource type="PackedScene" 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"] diff --git a/game/addons/zylann.hterrain/tools/generator/shaders/bump2normal.gdshader b/game/addons/zylann.hterrain/tools/generator/shaders/bump2normal.gdshader new file mode 100644 index 0000000..7dd9714 --- /dev/null +++ b/game/addons/zylann.hterrain/tools/generator/shaders/bump2normal.gdshader @@ -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); +} + diff --git a/game/addons/zylann.hterrain/tools/generator/shaders/erode.gdshader b/game/addons/zylann.hterrain/tools/generator/shaders/erode.gdshader new file mode 100644 index 0000000..6f45e1c --- /dev/null +++ b/game/addons/zylann.hterrain/tools/generator/shaders/erode.gdshader @@ -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); +} diff --git a/game/addons/zylann.hterrain/tools/generator/shaders/perlin_noise.gdshader b/game/addons/zylann.hterrain/tools/generator/shaders/perlin_noise.gdshader new file mode 100644 index 0000000..7609f7c --- /dev/null +++ b/game/addons/zylann.hterrain/tools/generator/shaders/perlin_noise.gdshader @@ -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); +} diff --git a/game/addons/zylann.hterrain/tools/generator/texture_generator.gd b/game/addons/zylann.hterrain/tools/generator/texture_generator.gd new file mode 100644 index 0000000..0caaf7c --- /dev/null +++ b/game/addons/zylann.hterrain/tools/generator/texture_generator.gd @@ -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 + }) + diff --git a/game/addons/zylann.hterrain/tools/generator/texture_generator_pass.gd b/game/addons/zylann.hterrain/tools/generator/texture_generator_pass.gd new file mode 100644 index 0000000..76745f1 --- /dev/null +++ b/game/addons/zylann.hterrain/tools/generator/texture_generator_pass.gd @@ -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 |