aboutsummaryrefslogtreecommitdiff
path: root/game/addons/zylann.hterrain/tools/generator/texture_generator.gd
blob: 0caaf7c680e64300af860331e5ff86b5fda663ed (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
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
   })