first commit
This commit is contained in:
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
|
||||
Reference in New Issue
Block a user