aboutsummaryrefslogtreecommitdiff
path: root/game/addons/zylann.hterrain/tools/normalmap_baker.gd
diff options
context:
space:
mode:
Diffstat (limited to 'game/addons/zylann.hterrain/tools/normalmap_baker.gd')
-rw-r--r--game/addons/zylann.hterrain/tools/normalmap_baker.gd148
1 files changed, 148 insertions, 0 deletions
diff --git a/game/addons/zylann.hterrain/tools/normalmap_baker.gd b/game/addons/zylann.hterrain/tools/normalmap_baker.gd
new file mode 100644
index 0000000..e5af37d
--- /dev/null
+++ b/game/addons/zylann.hterrain/tools/normalmap_baker.gd
@@ -0,0 +1,148 @@
+
+# Bakes normals asynchronously in the editor as the heightmap gets modified.
+# It uses the heightmap texture to change the normalmap image, which is then uploaded like an edit.
+# This is probably not a nice method GPU-wise, but it's way faster than GDScript.
+
+@tool
+extends Node
+
+const HTerrainData = preload("../hterrain_data.gd")
+
+const VIEWPORT_SIZE = 64
+
+const STATE_PENDING = 0
+const STATE_PROCESSING = 1
+
+var _viewport : SubViewport = null
+var _ci : Sprite2D = null
+var _pending_tiles_grid := {}
+var _pending_tiles_queue := []
+var _processing_tile = null
+var _terrain_data : HTerrainData = null
+
+
+func _init():
+ assert(VIEWPORT_SIZE <= HTerrainData.MIN_RESOLUTION)
+ _viewport = SubViewport.new()
+ _viewport.size = Vector2(VIEWPORT_SIZE + 2, VIEWPORT_SIZE + 2)
+ _viewport.render_target_update_mode = SubViewport.UPDATE_DISABLED
+ _viewport.render_target_clear_mode = SubViewport.CLEAR_MODE_ALWAYS
+ # We only render 2D, but we don't want the parent world to interfere
+ _viewport.world_3d = World3D.new()
+ _viewport.own_world_3d = true
+ add_child(_viewport)
+
+ var mat = ShaderMaterial.new()
+ mat.shader = load("res://addons/zylann.hterrain/tools/bump2normal_tex.gdshader")
+
+ _ci = Sprite2D.new()
+ _ci.centered = false
+ _ci.material = mat
+ _viewport.add_child(_ci)
+
+ set_process(false)
+
+
+func set_terrain_data(data: HTerrainData):
+ if data == _terrain_data:
+ return
+
+ _pending_tiles_grid.clear()
+ _pending_tiles_queue.clear()
+ _processing_tile = null
+ _ci.texture = null
+ set_process(false)
+
+ if data == null:
+ _terrain_data.map_changed.disconnect(_on_terrain_data_map_changed)
+ _terrain_data.resolution_changed.disconnect(_on_terrain_data_resolution_changed)
+
+ _terrain_data = data
+
+ if _terrain_data != null:
+ _terrain_data.map_changed.connect(_on_terrain_data_map_changed)
+ _terrain_data.resolution_changed.connect(_on_terrain_data_resolution_changed)
+ _ci.texture = data.get_texture(HTerrainData.CHANNEL_HEIGHT)
+
+
+func _on_terrain_data_map_changed(maptype: int, index: int):
+ if maptype == HTerrainData.CHANNEL_HEIGHT:
+ _ci.texture = _terrain_data.get_texture(HTerrainData.CHANNEL_HEIGHT)
+
+
+func _on_terrain_data_resolution_changed():
+ # TODO Workaround issue https://github.com/godotengine/godot/issues/24463
+ _ci.queue_redraw()
+
+
+# TODO Use Vector2i
+func request_tiles_in_region(min_pos: Vector2, size: Vector2):
+ assert(is_inside_tree())
+ assert(_terrain_data != null)
+ var res = _terrain_data.get_resolution()
+
+ min_pos -= Vector2(1, 1)
+ var max_pos = min_pos + size + Vector2(1, 1)
+ var tmin = (min_pos / VIEWPORT_SIZE).floor()
+ var tmax = (max_pos / VIEWPORT_SIZE).ceil()
+ var ntx = res / VIEWPORT_SIZE
+ var nty = res / VIEWPORT_SIZE
+ tmin.x = clamp(tmin.x, 0, ntx)
+ tmin.y = clamp(tmin.y, 0, nty)
+ tmax.x = clamp(tmax.x, 0, ntx)
+ tmax.y = clamp(tmax.y, 0, nty)
+
+ for y in range(tmin.y, tmax.y):
+ for x in range(tmin.x, tmax.x):
+ request_tile(Vector2(x, y))
+
+
+# TODO Use Vector2i
+func request_tile(tpos: Vector2):
+ assert(tpos == tpos.round())
+ if _pending_tiles_grid.has(tpos):
+ var state = _pending_tiles_grid[tpos]
+ if state == STATE_PENDING:
+ return
+ _pending_tiles_grid[tpos] = STATE_PENDING
+ _pending_tiles_queue.push_front(tpos)
+ set_process(true)
+
+
+func _process(delta):
+ if not is_processing():
+ return
+
+ if _processing_tile != null and _terrain_data != null:
+ var src = _viewport.get_texture().get_image()
+ var dst = _terrain_data.get_image(HTerrainData.CHANNEL_NORMAL)
+
+ src.convert(dst.get_format())
+ #src.save_png(str("test_", _processing_tile.x, "_", _processing_tile.y, ".png"))
+ var pos = _processing_tile * VIEWPORT_SIZE
+ var w = src.get_width() - 1
+ var h = src.get_height() - 1
+ dst.blit_rect(src, Rect2i(1, 1, w, h), pos)
+ _terrain_data.notify_region_change(Rect2(pos.x, pos.y, w, h), HTerrainData.CHANNEL_NORMAL)
+
+ if _pending_tiles_grid[_processing_tile] == STATE_PROCESSING:
+ _pending_tiles_grid.erase(_processing_tile)
+ _processing_tile = null
+
+ if _has_pending_tiles():
+ var tpos = _pending_tiles_queue[-1]
+ _pending_tiles_queue.pop_back()
+ # The sprite will be much larger than the viewport due to the size of the heightmap.
+ # We move it around so the part inside the viewport will correspond to the tile.
+ _ci.position = -VIEWPORT_SIZE * tpos + Vector2(1, 1)
+ _viewport.render_target_update_mode = SubViewport.UPDATE_ONCE
+ _processing_tile = tpos
+ _pending_tiles_grid[tpos] = STATE_PROCESSING
+ else:
+ set_process(false)
+
+
+func _has_pending_tiles():
+ return len(_pending_tiles_queue) > 0
+
+