aboutsummaryrefslogtreecommitdiff
path: root/game/addons/zylann.hterrain/tools/generator/texture_generator.gd
diff options
context:
space:
mode:
Diffstat (limited to 'game/addons/zylann.hterrain/tools/generator/texture_generator.gd')
-rw-r--r--game/addons/zylann.hterrain/tools/generator/texture_generator.gd331
1 files changed, 331 insertions, 0 deletions
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
+ })
+