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
|
# 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
|