project_teddy/addons/zylann.hterrain/tools/generator/texture_generator.gd

277 lines
7.8 KiB
GDScript

# 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
var _passes := []
var _resolution := Vector2i(512, 512)
var _output_padding := [0, 0, 0, 0]
var _viewport : SubViewport = null
var _ci : TextureRect = null
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():
assert(_viewport == null)
assert(_ci == null)
_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
add_child(_viewport)
_dummy_texture = load(DUMMY_TEXTURE_PATH)
if _dummy_texture == null:
_logger.error(str("Failed to load dummy texture ", DUMMY_TEXTURE_PATH))
_ci = TextureRect.new()
_ci.stretch_mode = TextureRect.STRETCH_SCALE
_ci.texture = _dummy_texture
_viewport.add_child(_ci)
_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
assert(_viewport != null)
assert(_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)
_ci.size = padded_size
_viewport.size = padded_size
_viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
_viewport.render_target_clear_mode = SubViewport.CLEAR_MODE_ONCE
_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
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)
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:
_viewport.render_target_update_mode = SubViewport.UPDATE_DISABLED
set_process(false)
return
var p : HT_TextureGeneratorPass = _running_passes[_running_pass_index]
if _running_iteration == 0:
_setup_pass(p)
_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
# The viewport should render after the tree was processed
func _setup_pass(p: HT_TextureGeneratorPass):
if p.texture != null:
_ci.texture = p.texture
else:
_ci.texture = _dummy_texture
if p.shader != null:
if _shader_material == null:
_shader_material = ShaderMaterial.new()
_shader_material.shader = p.shader
_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(_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:
_ci.material = null
if p.clear:
_viewport.render_target_clear_mode = SubViewport.CLEAR_MODE_ONCE
func _create_output_image(metadata):
var tex := _viewport.get_texture()
var src := tex.get_image()
# 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
})