diff options
Diffstat (limited to 'game/addons/zylann.hterrain/tools/generator/texture_generator.gd')
-rw-r--r-- | game/addons/zylann.hterrain/tools/generator/texture_generator.gd | 331 |
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 + }) + |