└── addons └── zylann.debug_draw ├── Hack-Regular.ttf ├── CHANGELOG.md ├── Hack-Regular.ttf.import ├── LICENSE.md └── debug_draw.gd /addons/zylann.debug_draw/Hack-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zylann/godot_debug_draw/HEAD/addons/zylann.debug_draw/Hack-Regular.ttf -------------------------------------------------------------------------------- /addons/zylann.debug_draw/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ============= 3 | 4 | 5 | 0.2 6 | ---- 7 | 8 | Uses Godot 4 from this version. 9 | 10 | - Added `draw_cube` 11 | - Added `draw_transformed_cube` 12 | - Added `draw_axes` 13 | - Added `draw_mesh` 14 | - Cubes and boxes are now centered 15 | - A monospace font is now used instead of Godot's default font 16 | - Fix 2D drawing order if the game uses multiple `CanvasLayer` (thanks AheadGameStudio) 17 | - Fix leak in headless mode where shapes were never cleared due to the frame counter never increasing 18 | 19 | 20 | 0.1 21 | ----- 22 | 23 | Initial version 24 | 25 | - Functions to display text as a HUD 26 | - Functions to draw 3D lines 27 | - Functions to draw 3D boxes 28 | -------------------------------------------------------------------------------- /addons/zylann.debug_draw/Hack-Regular.ttf.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="font_data_dynamic" 4 | type="FontFile" 5 | uid="uid://ir58tc8kwot7" 6 | path="res://.godot/imported/Hack-Regular.ttf-85c0e00233988dd4f27e5d86a4074d34.fontdata" 7 | 8 | [deps] 9 | 10 | source_file="res://addons/zylann.debug_draw/Hack-Regular.ttf" 11 | dest_files=["res://.godot/imported/Hack-Regular.ttf-85c0e00233988dd4f27e5d86a4074d34.fontdata"] 12 | 13 | [params] 14 | 15 | Rendering=null 16 | antialiased=true 17 | generate_mipmaps=false 18 | multichannel_signed_distance_field=false 19 | msdf_pixel_range=8 20 | msdf_size=48 21 | force_autohinter=false 22 | hinting=1 23 | subpixel_positioning=1 24 | oversampling=0.0 25 | Fallbacks=null 26 | fallbacks=[] 27 | Compress=null 28 | compress=true 29 | preload=[] 30 | language_support={} 31 | script_support={} 32 | opentype_features={} 33 | -------------------------------------------------------------------------------- /addons/zylann.debug_draw/LICENSE.md: -------------------------------------------------------------------------------- 1 | DebugDraw utility for Godot Engine 2 | -------------------------------- 3 | 4 | Copyright (c) 2020 Marc Gilleron 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | 12 | -------------------------------------------------------------------------------- /addons/zylann.debug_draw/debug_draw.gd: -------------------------------------------------------------------------------- 1 | 2 | ## @brief Single-file autoload for debug drawing and printing. 3 | ## Draw and print on screen from anywhere in a single line of code. 4 | ## Find it quickly by naming it "DDD". 5 | 6 | # TODO Thread-safety 7 | # TODO 2D functions 8 | 9 | extends CanvasLayer 10 | 11 | const DebugDrawFont = preload("res://addons/zylann.debug_draw/Hack-Regular.ttf") 12 | 13 | ## @brief How many frames HUD text lines remain shown after being invoked. 14 | const TEXT_LINGER_FRAMES = 5 15 | ## @brief How many frames lines remain shown after being drawn. 16 | const LINES_LINGER_FRAMES = 1 17 | ## @brief Color of the text drawn as HUD 18 | const TEXT_COLOR = Color.WHITE 19 | ## @brief Background color of the text drawn as HUD 20 | const TEXT_BG_COLOR = Color(0.3, 0.3, 0.3, 0.8) 21 | ## @brief font size used for debug text 22 | const TEXT_SIZE = 12 23 | 24 | # Can't use `Engine.get_frames_drawn` because it is always zero in headless mode. 25 | var _frame_counter := 0 26 | 27 | # 2D 28 | 29 | var _canvas_item : CanvasItem = null 30 | var _texts := {} 31 | 32 | # 3D 33 | 34 | var _boxes := [] 35 | var _box_pool := [] 36 | var _box_mesh : Mesh = null 37 | var _line_material_pool := [] 38 | 39 | var _lines := [] 40 | var _line_immediate_mesh : ImmediateMesh 41 | 42 | var _mesh_instances := [] 43 | var _mesh_instance_pool := [] 44 | var _mesh_material_pool := [] 45 | 46 | 47 | func _ready(): 48 | # Always process even if the game is paused 49 | process_mode = Node.PROCESS_MODE_ALWAYS 50 | # Draw 2D on top of every other CanvasLayer 51 | layer = 100 52 | _line_immediate_mesh = ImmediateMesh.new() 53 | var immediate_mesh_instance = MeshInstance3D.new() 54 | immediate_mesh_instance.material_override = _get_line_material() 55 | immediate_mesh_instance.mesh = _line_immediate_mesh 56 | add_child(immediate_mesh_instance) 57 | 58 | 59 | ## @brief Draws the unshaded outline of a 3D cube. 60 | ## @param position: world-space position of the center of the cube 61 | ## @param size: size of the cube in world units 62 | ## @param color 63 | ## @param linger_frames: optionally makes the box remain drawn for longer 64 | func draw_cube(position: Vector3, size: float, color: Color = Color.WHITE, linger := 0): 65 | draw_box(position, Vector3(size, size, size), color, linger) 66 | 67 | 68 | ## @brief Draws the unshaded outline of a 3D box. 69 | ## @param position: world-space position of the center of the box 70 | ## @param size: size of the box in world units 71 | ## @param color 72 | ## @param linger_frames: optionally makes the box remain drawn for longer 73 | func draw_box(position: Vector3, size: Vector3, color: Color = Color.WHITE, linger_frames = 0): 74 | var mi := _get_box() 75 | var mat := _get_line_material() 76 | mat.albedo_color = color 77 | mi.material_override = mat 78 | mi.position = position 79 | mi.scale = size 80 | _boxes.append({ 81 | "node": mi, 82 | "frame": _frame_counter + LINES_LINGER_FRAMES + linger_frames 83 | }) 84 | 85 | 86 | ## @brief Draws the unshaded outline of a 3D transformed cube. 87 | ## @param trans: transform of the cube. The basis defines its size. 88 | ## @param color 89 | func draw_transformed_cube(trans: Transform3D, color: Color = Color.WHITE): 90 | var mi := _get_box() 91 | var mat := _get_line_material() 92 | mat.albedo_color = color 93 | mi.material_override = mat 94 | mi.transform = Transform3D(trans.basis, trans.origin) 95 | _boxes.append({ 96 | "node": mi, 97 | "frame": _frame_counter + LINES_LINGER_FRAMES 98 | }) 99 | 100 | 101 | ## @brief Draws the basis of the given transform using 3 lines 102 | ## of color red for X, green for Y, and blue for Z. 103 | ## @param transform_ 104 | ## @param scale_: extra scale applied on top of the transform 105 | func draw_axes(transform_: Transform3D, scale_ = 1.0): 106 | draw_ray_3d(transform_.origin, transform_.basis.x, scale_, Color(1,0,0)) 107 | draw_ray_3d(transform_.origin, transform_.basis.y, scale_, Color(0,1,0)) 108 | draw_ray_3d(transform_.origin, transform_.basis.z, scale_, Color(0,0,1)) 109 | 110 | 111 | ## @brief Draws a mesh at the specified transform. 112 | ## If the mesh's first surface uses line or point primitive, 113 | ## it is drawn using an unshaded material. 114 | ## @param transform_ 115 | ## @param color: tint of the mesh. 116 | func draw_mesh(mesh: Mesh, transform_: Transform3D, color := Color.WHITE): 117 | var mi := _get_mesh_instance() 118 | # TODO How do I get the primitive type used by the mesh? 119 | # Why can Mesh have virtual methods to implement that, 120 | # but no callable method to actually GET that? 121 | var mat : Material 122 | var uses_lines = false 123 | if mesh is ArrayMesh: 124 | var pt : int = mesh.surface_get_primitive_type(0) 125 | if pt == Mesh.PRIMITIVE_LINES or pt == Mesh.PRIMITIVE_LINE_STRIP or \ 126 | pt == Mesh.PRIMITIVE_POINTS: 127 | mat = _get_line_material() 128 | uses_lines = true 129 | else: 130 | mat = _get_mesh_material() 131 | else: 132 | mat = _get_mesh_material() 133 | mat.albedo_color = color 134 | mi.material_override = mat 135 | mi.transform = transform_ 136 | mi.mesh = mesh 137 | _mesh_instances.append({ 138 | "node": mi, 139 | "uses_lines": uses_lines, 140 | "frame": _frame_counter + LINES_LINGER_FRAMES 141 | }) 142 | 143 | 144 | ## @brief Draws the unshaded outline of a 3D box. 145 | ## @param aabb: world-space box to draw as an AABB 146 | ## @param color 147 | ## @param linger_frames: optionally makes the box remain drawn for longer 148 | func draw_box_aabb(aabb: AABB, color = Color.WHITE, linger_frames = 0): 149 | var mi := _get_box() 150 | var mat := _get_line_material() 151 | mat.albedo_color = color 152 | mi.material_override = mat 153 | mi.position = aabb.get_center() 154 | mi.scale = aabb.size 155 | _boxes.append({ 156 | "node": mi, 157 | "frame": _frame_counter + LINES_LINGER_FRAMES + linger_frames 158 | }) 159 | 160 | 161 | ## @brief Draws an unshaded 3D line. 162 | ## @param a: begin position in world units 163 | ## @param b: end position in world units 164 | ## @param color 165 | func draw_line_3d(a: Vector3, b: Vector3, color: Color): 166 | _lines.append([ 167 | a, b, color, 168 | _frame_counter + LINES_LINGER_FRAMES, 169 | ]) 170 | 171 | 172 | ## @brief Draws an unshaded 3D line defined as a ray. 173 | ## @param origin: begin position in world units 174 | ## @param direction 175 | ## @param length: length of the line in world units 176 | ## @param color 177 | func draw_ray_3d(origin: Vector3, direction: Vector3, length: float, color : Color): 178 | draw_line_3d(origin, origin + direction * length, color) 179 | 180 | 181 | ## @brief Adds a text monitoring line to the HUD, from the provided value. 182 | ## It will be shown as such: - {key}: {text} 183 | ## Multiple calls with the same `key` will override previous text. 184 | ## @param key: identifier of the line 185 | ## @param text: text to show next to the key 186 | func set_text(key: String, value=""): 187 | _texts[key] = { 188 | "text": value if typeof(value) == TYPE_STRING else str(value), 189 | "frame": _frame_counter + TEXT_LINGER_FRAMES 190 | } 191 | 192 | 193 | func _get_box() -> MeshInstance3D: 194 | var mi : MeshInstance3D 195 | if len(_box_pool) == 0: 196 | mi = MeshInstance3D.new() 197 | if _box_mesh == null: 198 | _box_mesh = _create_wirecube_mesh(Color.WHITE) 199 | mi.mesh = _box_mesh 200 | add_child(mi) 201 | else: 202 | mi = _box_pool[-1] 203 | _box_pool.pop_back() 204 | return mi 205 | 206 | 207 | func _recycle_box(mi: MeshInstance3D): 208 | mi.hide() 209 | _box_pool.append(mi) 210 | 211 | 212 | func _get_line_material() -> StandardMaterial3D: 213 | var mat : StandardMaterial3D 214 | if len(_line_material_pool) == 0: 215 | mat = StandardMaterial3D.new() 216 | mat.flags_unshaded = true 217 | mat.vertex_color_use_as_albedo = true 218 | else: 219 | mat = _line_material_pool[-1] 220 | _line_material_pool.pop_back() 221 | return mat 222 | 223 | 224 | func _recycle_line_material(mat: StandardMaterial3D): 225 | _line_material_pool.append(mat) 226 | 227 | 228 | func _get_mesh_instance() -> MeshInstance3D: 229 | var mi : MeshInstance3D 230 | if len(_mesh_instance_pool) == 0: 231 | mi = MeshInstance3D.new() 232 | add_child(mi) 233 | else: 234 | mi = _mesh_instance_pool[-1] 235 | _mesh_instance_pool.pop_back() 236 | return mi 237 | 238 | 239 | func _recycle_mesh_instance(mi: MeshInstance3D): 240 | mi.hide() 241 | _mesh_instance_pool.append(mi) 242 | 243 | 244 | func _get_mesh_material() -> StandardMaterial3D: 245 | var mat : StandardMaterial3D 246 | if len(_mesh_material_pool) == 0: 247 | mat = StandardMaterial3D.new() 248 | else: 249 | mat = _mesh_material_pool[-1] 250 | _mesh_material_pool.pop_back() 251 | return mat 252 | 253 | 254 | func _recycle_mesh_material(mat: StandardMaterial3D): 255 | _mesh_material_pool.append(mat) 256 | 257 | 258 | func _process(_unused_delta: float): 259 | _frame_counter += 1 260 | 261 | _process_boxes() 262 | _process_lines() 263 | _process_canvas() 264 | _process_meshes() 265 | 266 | 267 | func _process_3d_boxes_delayed_free(items: Array): 268 | var i := 0 269 | while i < len(items): 270 | var d = items[i] 271 | if d.frame <= _frame_counter: 272 | _recycle_line_material(d.node.material_override) 273 | d.node.queue_free() 274 | items[i] = items[len(items) - 1] 275 | items.pop_back() 276 | else: 277 | i += 1 278 | 279 | 280 | func _process_boxes(): 281 | _process_3d_boxes_delayed_free(_boxes) 282 | 283 | # Progressively delete boxes in pool 284 | if len(_box_pool) > 0: 285 | var last = _box_pool[-1] 286 | _box_pool.pop_back() 287 | last.queue_free() 288 | 289 | 290 | func _process_mesh_instance_delayed_free(items: Array): 291 | var i := 0 292 | while i < len(items): 293 | var d = items[i] 294 | if d.frame <= _frame_counter: 295 | if d.uses_lines: 296 | _recycle_line_material(d.node.material_override) 297 | else: 298 | _recycle_mesh_material(d.node.material_override) 299 | d.node.queue_free() 300 | items[i] = items[len(items) - 1] 301 | items.pop_back() 302 | else: 303 | i += 1 304 | 305 | 306 | func _process_meshes(): 307 | _process_mesh_instance_delayed_free(_mesh_instances) 308 | 309 | 310 | func _process_lines(): 311 | var im := _line_immediate_mesh 312 | im.clear_surfaces() 313 | 314 | if len(_lines) == 0: 315 | return 316 | 317 | im.surface_begin(Mesh.PRIMITIVE_LINES) 318 | 319 | for line in _lines: 320 | var p1 : Vector3 = line[0] 321 | var p2 : Vector3 = line[1] 322 | var color : Color = line[2] 323 | 324 | im.surface_set_color(color) 325 | im.surface_add_vertex(p1) 326 | im.surface_add_vertex(p2) 327 | 328 | im.surface_end() 329 | 330 | # Delayed removal 331 | var i := 0 332 | while i < len(_lines): 333 | var item = _lines[i] 334 | var frame = item[3] 335 | if frame <= _frame_counter: 336 | _lines[i] = _lines[len(_lines) - 1] 337 | _lines.pop_back() 338 | else: 339 | i += 1 340 | 341 | 342 | func _process_canvas(): 343 | # Remove text lines after some time 344 | for key in _texts.keys(): 345 | var t = _texts[key] 346 | if t.frame <= _frame_counter: 347 | _texts.erase(key) 348 | 349 | # Update canvas 350 | if _canvas_item == null: 351 | _canvas_item = Node2D.new() 352 | _canvas_item.position = Vector2(8, 8) 353 | _canvas_item.draw.connect(_on_CanvasItem_draw) 354 | add_child(_canvas_item) 355 | _canvas_item.queue_redraw() 356 | 357 | 358 | func _on_CanvasItem_draw(): 359 | var ci := _canvas_item 360 | 361 | var font := DebugDrawFont 362 | 363 | var ascent := Vector2(0, font.get_ascent()) 364 | var pos := Vector2() 365 | var xpad := 2 366 | var ypad := 1 367 | var font_offset := ascent + Vector2(xpad, ypad) 368 | var line_height := font.get_height() + 2 * ypad 369 | 370 | for key in _texts.keys(): 371 | var t = _texts[key] 372 | var text := str(key, ": ", t.text) 373 | var ss := font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1, TEXT_SIZE) 374 | ci.draw_rect(Rect2(pos, Vector2(ss.x + xpad * 2, line_height)), TEXT_BG_COLOR) 375 | ci.draw_string(font, pos + font_offset, text, HORIZONTAL_ALIGNMENT_LEFT, -1, TEXT_SIZE, 376 | TEXT_COLOR) 377 | pos.y += line_height 378 | 379 | 380 | static func _create_wirecube_mesh(color := Color.WHITE) -> ArrayMesh: 381 | var n = -0.5 382 | var p = 0.5 383 | var positions := PackedVector3Array([ 384 | Vector3(n, n, n), 385 | Vector3(p, n, n), 386 | Vector3(p, n, p), 387 | Vector3(n, n, p), 388 | Vector3(n, p, n), 389 | Vector3(p, p, n), 390 | Vector3(p, p, p), 391 | Vector3(n, p, p) 392 | ]) 393 | var colors := PackedColorArray([ 394 | color, color, color, color, 395 | color, color, color, color, 396 | ]) 397 | var indices := PackedInt32Array([ 398 | 0, 1, 399 | 1, 2, 400 | 2, 3, 401 | 3, 0, 402 | 403 | 4, 5, 404 | 5, 6, 405 | 6, 7, 406 | 7, 4, 407 | 408 | 0, 4, 409 | 1, 5, 410 | 2, 6, 411 | 3, 7 412 | ]) 413 | var arrays := [] 414 | arrays.resize(Mesh.ARRAY_MAX) 415 | arrays[Mesh.ARRAY_VERTEX] = positions 416 | arrays[Mesh.ARRAY_COLOR] = colors 417 | arrays[Mesh.ARRAY_INDEX] = indices 418 | var mesh := ArrayMesh.new() 419 | mesh.add_surface_from_arrays(Mesh.PRIMITIVE_LINES, arrays) 420 | return mesh 421 | --------------------------------------------------------------------------------