-
Notifications
You must be signed in to change notification settings - Fork 15
/
debug_draw.gd
416 lines (337 loc) · 10.8 KB
/
debug_draw.gd
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
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
## @brief Single-file autoload for debug drawing and printing.
## Draw and print on screen from anywhere in a single line of code.
## Find it quickly by naming it "DDD".
# TODO Thread-safety
# TODO 2D functions
extends CanvasLayer
const DebugDrawFont = preload("res://addons/zylann.debug_draw/Hack-Regular.ttf")
## @brief How many frames HUD text lines remain shown after being invoked.
const TEXT_LINGER_FRAMES = 5
## @brief How many frames lines remain shown after being drawn.
const LINES_LINGER_FRAMES = 1
## @brief Color of the text drawn as HUD
const TEXT_COLOR = Color.WHITE
## @brief Background color of the text drawn as HUD
const TEXT_BG_COLOR = Color(0.3, 0.3, 0.3, 0.8)
## @brief font size used for debug text
const TEXT_SIZE = 12
# 2D
var _canvas_item : CanvasItem = null
var _texts := {}
# 3D
var _boxes := []
var _box_pool := []
var _box_mesh : Mesh = null
var _line_material_pool := []
var _lines := []
var _line_immediate_mesh : ImmediateMesh
var _mesh_instances := []
var _mesh_instance_pool := []
var _mesh_material_pool := []
func _ready():
# Always process even if the game is paused
process_mode = Node.PROCESS_MODE_ALWAYS
# Draw 2D on top of every other CanvasLayer
layer = 100
_line_immediate_mesh = ImmediateMesh.new()
var immediate_mesh_instance = MeshInstance3D.new()
immediate_mesh_instance.material_override = _get_line_material()
immediate_mesh_instance.mesh = _line_immediate_mesh
add_child(immediate_mesh_instance)
## @brief Draws the unshaded outline of a 3D cube.
## @param position: world-space position of the center of the cube
## @param size: size of the cube in world units
## @param color
## @param linger_frames: optionally makes the box remain drawn for longer
func draw_cube(position: Vector3, size: float, color: Color = Color.WHITE, linger := 0):
draw_box(position, Vector3(size, size, size), color, linger)
## @brief Draws the unshaded outline of a 3D box.
## @param position: world-space position of the center of the box
## @param size: size of the box in world units
## @param color
## @param linger_frames: optionally makes the box remain drawn for longer
func draw_box(position: Vector3, size: Vector3, color: Color = Color.WHITE, linger_frames = 0):
var mi := _get_box()
var mat := _get_line_material()
mat.albedo_color = color
mi.material_override = mat
mi.position = position
mi.scale = size
_boxes.append({
"node": mi,
"frame": Engine.get_frames_drawn() + LINES_LINGER_FRAMES + linger_frames
})
## @brief Draws the unshaded outline of a 3D transformed cube.
## @param trans: transform of the cube. The basis defines its size.
## @param color
func draw_transformed_cube(trans: Transform3D, color: Color = Color.WHITE):
var mi := _get_box()
var mat := _get_line_material()
mat.albedo_color = color
mi.material_override = mat
mi.transform = Transform3D(trans.basis, trans.origin)
_boxes.append({
"node": mi,
"frame": Engine.get_frames_drawn() + LINES_LINGER_FRAMES
})
## @brief Draws the basis of the given transform using 3 lines
## of color red for X, green for Y, and blue for Z.
## @param transform_
## @param scale_: extra scale applied on top of the transform
func draw_axes(transform_: Transform3D, scale_ = 1.0):
draw_ray_3d(transform_.origin, transform_.basis.x, scale_, Color(1,0,0))
draw_ray_3d(transform_.origin, transform_.basis.y, scale_, Color(0,1,0))
draw_ray_3d(transform_.origin, transform_.basis.z, scale_, Color(0,0,1))
## @brief Draws a mesh at the specified transform.
## If the mesh's first surface uses line or point primitive,
## it is drawn using an unshaded material.
## @param transform_
## @param color: tint of the mesh.
func draw_mesh(mesh: Mesh, transform_: Transform3D, color := Color.WHITE):
var mi := _get_mesh_instance()
# TODO How do I get the primitive type used by the mesh?
# Why can Mesh have virtual methods to implement that,
# but no callable method to actually GET that?
var mat : Material
var uses_lines = false
if mesh is ArrayMesh:
var pt : int = mesh.surface_get_primitive_type(0)
if pt == Mesh.PRIMITIVE_LINES or pt == Mesh.PRIMITIVE_LINE_STRIP or \
pt == Mesh.PRIMITIVE_POINTS:
mat = _get_line_material()
uses_lines = true
else:
mat = _get_mesh_material()
else:
mat = _get_mesh_material()
mat.albedo_color = color
mi.material_override = mat
mi.transform = transform_
mi.mesh = mesh
_mesh_instances.append({
"node": mi,
"uses_lines": uses_lines,
"frame": Engine.get_frames_drawn() + LINES_LINGER_FRAMES
})
## @brief Draws the unshaded outline of a 3D box.
## @param aabb: world-space box to draw as an AABB
## @param color
## @param linger_frames: optionally makes the box remain drawn for longer
func draw_box_aabb(aabb: AABB, color = Color.WHITE, linger_frames = 0):
var mi := _get_box()
var mat := _get_line_material()
mat.albedo_color = color
mi.material_override = mat
mi.position = aabb.get_center()
mi.scale = aabb.size
_boxes.append({
"node": mi,
"frame": Engine.get_frames_drawn() + LINES_LINGER_FRAMES + linger_frames
})
## @brief Draws an unshaded 3D line.
## @param a: begin position in world units
## @param b: end position in world units
## @param color
func draw_line_3d(a: Vector3, b: Vector3, color: Color):
_lines.append([
a, b, color,
Engine.get_frames_drawn() + LINES_LINGER_FRAMES,
])
## @brief Draws an unshaded 3D line defined as a ray.
## @param origin: begin position in world units
## @param direction
## @param length: length of the line in world units
## @param color
func draw_ray_3d(origin: Vector3, direction: Vector3, length: float, color : Color):
draw_line_3d(origin, origin + direction * length, color)
## @brief Adds a text monitoring line to the HUD, from the provided value.
## It will be shown as such: - {key}: {text}
## Multiple calls with the same `key` will override previous text.
## @param key: identifier of the line
## @param text: text to show next to the key
func set_text(key: String, value=""):
_texts[key] = {
"text": value if typeof(value) == TYPE_STRING else str(value),
"frame": Engine.get_frames_drawn() + TEXT_LINGER_FRAMES
}
func _get_box() -> MeshInstance3D:
var mi : MeshInstance3D
if len(_box_pool) == 0:
mi = MeshInstance3D.new()
if _box_mesh == null:
_box_mesh = _create_wirecube_mesh(Color.WHITE)
mi.mesh = _box_mesh
add_child(mi)
else:
mi = _box_pool[-1]
_box_pool.pop_back()
return mi
func _recycle_box(mi: MeshInstance3D):
mi.hide()
_box_pool.append(mi)
func _get_line_material() -> StandardMaterial3D:
var mat : StandardMaterial3D
if len(_line_material_pool) == 0:
mat = StandardMaterial3D.new()
mat.flags_unshaded = true
mat.vertex_color_use_as_albedo = true
else:
mat = _line_material_pool[-1]
_line_material_pool.pop_back()
return mat
func _recycle_line_material(mat: StandardMaterial3D):
_line_material_pool.append(mat)
func _get_mesh_instance() -> MeshInstance3D:
var mi : MeshInstance3D
if len(_mesh_instance_pool) == 0:
mi = MeshInstance3D.new()
add_child(mi)
else:
mi = _mesh_instance_pool[-1]
_mesh_instance_pool.pop_back()
return mi
func _recycle_mesh_instance(mi: MeshInstance3D):
mi.hide()
_mesh_instance_pool.append(mi)
func _get_mesh_material() -> StandardMaterial3D:
var mat : StandardMaterial3D
if len(_mesh_material_pool) == 0:
mat = StandardMaterial3D.new()
else:
mat = _mesh_material_pool[-1]
_mesh_material_pool.pop_back()
return mat
func _recycle_mesh_material(mat: StandardMaterial3D):
_mesh_material_pool.append(mat)
func _process(_delta: float):
_process_boxes()
_process_lines()
_process_canvas()
_process_meshes()
func _process_3d_boxes_delayed_free(items: Array):
var i := 0
while i < len(items):
var d = items[i]
if d.frame <= Engine.get_frames_drawn():
_recycle_line_material(d.node.material_override)
d.node.queue_free()
items[i] = items[len(items) - 1]
items.pop_back()
else:
i += 1
func _process_boxes():
_process_3d_boxes_delayed_free(_boxes)
# Progressively delete boxes in pool
if len(_box_pool) > 0:
var last = _box_pool[-1]
_box_pool.pop_back()
last.queue_free()
func _process_mesh_instance_delayed_free(items: Array):
var i := 0
while i < len(items):
var d = items[i]
if d.frame <= Engine.get_frames_drawn():
if d.uses_lines:
_recycle_line_material(d.node.material_override)
else:
_recycle_mesh_material(d.node.material_override)
d.node.queue_free()
items[i] = items[len(items) - 1]
items.pop_back()
else:
i += 1
func _process_meshes():
_process_mesh_instance_delayed_free(_mesh_instances)
func _process_lines():
var im := _line_immediate_mesh
im.clear_surfaces()
if len(_lines) == 0:
return
im.surface_begin(Mesh.PRIMITIVE_LINES)
for line in _lines:
var p1 : Vector3 = line[0]
var p2 : Vector3 = line[1]
var color : Color = line[2]
im.surface_set_color(color)
im.surface_add_vertex(p1)
im.surface_add_vertex(p2)
im.surface_end()
# Delayed removal
var i := 0
while i < len(_lines):
var item = _lines[i]
var frame = item[3]
if frame <= Engine.get_frames_drawn():
_lines[i] = _lines[len(_lines) - 1]
_lines.pop_back()
else:
i += 1
func _process_canvas():
# Remove text lines after some time
for key in _texts.keys():
var t = _texts[key]
if t.frame <= Engine.get_frames_drawn():
_texts.erase(key)
# Update canvas
if _canvas_item == null:
_canvas_item = Node2D.new()
_canvas_item.position = Vector2(8, 8)
_canvas_item.draw.connect(_on_CanvasItem_draw)
add_child(_canvas_item)
_canvas_item.queue_redraw()
func _on_CanvasItem_draw():
var ci := _canvas_item
var font := DebugDrawFont
var ascent := Vector2(0, font.get_ascent())
var pos := Vector2()
var xpad := 2
var ypad := 1
var font_offset := ascent + Vector2(xpad, ypad)
var line_height := font.get_height() + 2 * ypad
for key in _texts.keys():
var t = _texts[key]
var text := str(key, ": ", t.text)
var ss := font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1, TEXT_SIZE)
ci.draw_rect(Rect2(pos, Vector2(ss.x + xpad * 2, line_height)), TEXT_BG_COLOR)
ci.draw_string(font, pos + font_offset, text, HORIZONTAL_ALIGNMENT_LEFT, -1, TEXT_SIZE,
TEXT_COLOR)
pos.y += line_height
static func _create_wirecube_mesh(color := Color.WHITE) -> ArrayMesh:
var n = -0.5
var p = 0.5
var positions := PackedVector3Array([
Vector3(n, n, n),
Vector3(p, n, n),
Vector3(p, n, p),
Vector3(n, n, p),
Vector3(n, p, n),
Vector3(p, p, n),
Vector3(p, p, p),
Vector3(n, p, p)
])
var colors := PackedColorArray([
color, color, color, color,
color, color, color, color,
])
var indices := PackedInt32Array([
0, 1,
1, 2,
2, 3,
3, 0,
4, 5,
5, 6,
6, 7,
7, 4,
0, 4,
1, 5,
2, 6,
3, 7
])
var arrays := []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = positions
arrays[Mesh.ARRAY_COLOR] = colors
arrays[Mesh.ARRAY_INDEX] = indices
var mesh := ArrayMesh.new()
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_LINES, arrays)
return mesh