diff --git a/tutorials/2d/custom_drawing_in_2d.rst b/tutorials/2d/custom_drawing_in_2d.rst index ca073d185065..6706270576cf 100644 --- a/tutorials/2d/custom_drawing_in_2d.rst +++ b/tutorials/2d/custom_drawing_in_2d.rst @@ -6,21 +6,19 @@ Custom drawing in 2D Introduction ------------ -Godot has nodes to draw sprites, polygons, particles, and all sorts of -stuff. For most cases, this is enough. If there's no node to draw something specific -you need, you can make any 2D node (for example, :ref:`Control ` or -:ref:`Node2D ` based) draw custom commands. +Godot has nodes to draw sprites, polygons, particles, text, and many other +common game development needs. However, if you need something specific +not covered with the standard nodes you can make any 2D node (for example, +:ref:`Control ` or :ref:`Node2D `-based) +draw on screen using custom commands. Custom drawing in a 2D node is *really* useful. Here are some use cases: - Drawing shapes or logic that existing nodes can't do, such as an image with trails or a special animated polygon. -- Visualizations that are not that compatible with nodes, such as a - tetris board. (The tetris example uses a custom draw function to draw - the blocks.) -- Drawing a large number of simple objects. Custom drawing avoids the - overhead of using a large number of nodes, possibly lowering memory - usage and improving performance. +- Drawing a large number of simple objects, such as a grid or a board + for a 2d game. Custom drawing avoids the overhead of using a large number + of nodes, possibly lowering memory usage and improving performance. - Making a custom UI control. There are plenty of controls available, but when you have unusual needs, you will likely need a custom control. @@ -30,7 +28,8 @@ Drawing Add a script to any :ref:`CanvasItem ` derived node, like :ref:`Control ` or -:ref:`Node2D `. Then override the ``_draw()`` function. +:ref:`Node2D `. Then override the +:ref:`_draw()` function. .. tabs:: .. code-tab:: gdscript GDScript @@ -38,44 +37,49 @@ derived node, like :ref:`Control ` or extends Node2D func _draw(): - # Your draw commands here - pass + pass # Your draw commands here. .. code-tab:: csharp - public override void _Draw() + using Godot; + + public partial class MyNode2D : Node2D { - // Your draw commands here + public override void _Draw() + { + // Your draw commands here. + } } Draw commands are described in the :ref:`CanvasItem ` -class reference. There are plenty of them. +class reference. There are plenty of them and we will see some of them +in the examples below. Updating -------- -The ``_draw()`` function is only called once, and then the draw commands -are cached and remembered, so further calls are unnecessary. +The :ref:`_draw ` function is only called +once, and then the draw commands are cached and remembered, so further calls +are unnecessary. -If re-drawing is required because a state or something else changed, -call :ref:`CanvasItem.queue_redraw() ` +If re-drawing is required because a variable or something else changed, +call :ref:`CanvasItem.queue_redraw ` in that same node and a new ``_draw()`` call will happen. -Here is a little more complex example, a texture variable that will be -redrawn if modified: +Here is a little more complex example, where we have a texture variable +that can be modified at any time, and using a +:ref:`setter`, it forces a redraw +of the texture when modified: .. tabs:: .. code-tab:: gdscript GDScript extends Node2D - export (Texture) var texture setget _set_texture - - func _set_texture(value): - # If the texture variable is modified externally, - # this callback is called. - texture = value # Texture was changed. - queue_redraw() # Trigger a redraw of the node. + @export var texture : Texture2D: + set(value): + texture = value + queue_redraw() func _draw(): draw_texture(texture, Vector2()) @@ -86,8 +90,10 @@ redrawn if modified: public partial class MyNode2D : Node2D { - private Texture _texture; - public Texture Texture + private Texture2D _texture; + + [Export] + public Texture2D Texture { get { @@ -107,8 +113,15 @@ redrawn if modified: } } -In some cases, it may be desired to draw every frame. For this, -call ``queue_redraw()`` from the ``_process()`` callback, like this: +To see it in action, you can set the texture to be the Godot icon on the +editor by dragging and dropping the default ``icon.svg`` from the +``FileSystem`` tab to the Texture property on the ``Inspector`` tab. +When changing the ``Texture`` property value while the previous script is +running, the texture will also change automatically. + +In some cases, we may need to redraw every frame. For this, +call :ref:`queue_redraw ` +from the :ref:`_process ` method, like this: .. tabs:: .. code-tab:: gdscript GDScript @@ -116,8 +129,7 @@ call ``queue_redraw()`` from the ``_process()`` callback, like this: extends Node2D func _draw(): - # Your draw commands here - pass + pass # Your draw commands here. func _process(delta): queue_redraw() @@ -126,11 +138,11 @@ call ``queue_redraw()`` from the ``_process()`` callback, like this: using Godot; - public partial class CustomNode2D : Node2D + public partial class MyNode2D : Node2D { public override void _Draw() { - // Your draw commands here + // Your draw commands here. } public override void _Process(double delta) @@ -139,19 +151,20 @@ call ``queue_redraw()`` from the ``_process()`` callback, like this: } } -Coordinates ------------ +Coordinates and line width alignment +------------------------------------ The drawing API uses the CanvasItem's coordinate system, not necessarily pixel -coordinates. Which means it uses the coordinate space created after applying -the CanvasItem's transform. Additionally, you can apply a custom transform on -top of it by using +coordinates. This means ``_draw()`` uses the coordinate space created after +applying the CanvasItem's transform. Additionally, you can apply a custom +transform on top of it by using :ref:`draw_set_transform` or :ref:`draw_set_transform_matrix`. -When using ``draw_line``, you should consider the width of the line. -When using a width that is an odd size, the position should be shifted -by ``0.5`` to keep the line centered as shown below. +When using :ref:`draw_line `, you should +consider the width of the line. When using a width that is an odd size, the +position of the start and end points should be shifted by ``0.5`` to keep the +line centered, as shown below. .. image:: img/draw_line.png @@ -172,7 +185,8 @@ by ``0.5`` to keep the line centered as shown below. DrawLine(new Vector2(7.5f, 1.0f), new Vector2(7.5f, 4.0f), Colors.Green, 3.0f); } -The same applies to the ``draw_rect`` method with ``filled = false``. +The same applies to the :ref:`draw_rect ` +method with ``filled = false``. .. image:: img/draw_rect.png @@ -195,333 +209,850 @@ The same applies to the ``draw_rect`` method with ``filled = false``. DrawRect(new Rect2(16.0f, 2.0f, 3.0f, 3.0f), Colors.Green, false, 2.0f); } -An example: drawing circular arcs +Antialiased drawing +------------------- + +Godot offers method parameters in :ref:`draw_line` +to enable antialiasing, but not all custom drawing methods offer this ``antialiased`` +parameter. + +For custom drawing methods that don't provide an ``antialiased`` parameter, +you can enable 2D MSAA instead, which affects rendering in the entire viewport. +This provides high-quality antialiasing, but a higher performance cost and only +on specific elements. See :ref:`doc_2d_antialiasing` for more information. + +Here is a comparison of a line of minimal width (``width=-1``) drawn with +``antialiased=false``, ``antialiased=true``, and ``antialiased=false`` with +2D MSAA 2x, 4x, and 8x enabled. + +.. image:: img/draw_antialiasing_options.webp + +Tools +----- + +Drawing your own nodes might also be desired while running them in the +editor. This can be used as a preview or visualization of some feature or +behavior. + +To do this, you can use the :ref:`tool annotation` +on both GDScript and C#. See +:ref:`the example below` and +:ref:`doc_running_code_in_the_editor` for more information. + +.. _doc_draw_custom_example_1: + +Example 1: drawing a custom shape --------------------------------- We will now use the custom drawing functionality of the Godot Engine to draw -something that Godot doesn't provide functions for. As an example, Godot provides -a ``draw_circle()`` function that draws a whole circle. However, what about drawing a -portion of a circle? You will have to code a function to perform this and draw it yourself. +something that Godot doesn't provide functions for. We will recreate the Godot +logo but with code- only using drawing functions. -Arc function -^^^^^^^^^^^^ +You will have to code a function to perform this and draw it yourself. + +.. note:: -An arc is defined by its support circle parameters, that is, the center position -and the radius. The arc itself is then defined by the angle it starts from -and the angle at which it stops. These are the 4 arguments that we have to provide to our drawing function. -We'll also provide the color value, so we can draw the arc in different colors if we wish. + The following instructions use a fixed set of coordinates that could be too small + for high resolution screens (larger than 1080p). If that is your case, and the + drawing is too small consider increasing your window scale in + ``Menu > Project > Project settings > display/window/stretch/scale`` to adjust + the project to a higher resolution (a 2 or 4 scale tends to work well). -Basically, drawing a shape on the screen requires it to be decomposed into a certain number of points -linked from one to the next. As you can imagine, the more points your shape is made of, -the smoother it will appear, but the heavier it will also be in terms of processing cost. In general, -if your shape is huge (or in 3D, close to the camera), it will require more points to be drawn without -it being angular-looking. On the contrary, if your shape is small (or in 3D, far from the camera), -you may decrease its number of points to save processing costs; this is known as *Level of Detail (LOD)*. -In our example, we will simply use a fixed number of points, no matter the radius. +Drawing a custom polygon shape +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +While there is a dedicated node to draw custom polygons ( +:ref:`Polygon2D `), we will use in this case exclusively lower +level drawing functions to combine them on the same node and be able to create +more complex shapes later on. + +First, we will define a set of points -or X and Y coordinates- that will form +the base of our shape: .. tabs:: .. code-tab:: gdscript GDScript - func draw_circle_arc(center, radius, angle_from, angle_to, color): - var nb_points = 32 - var points_arc = PackedVector2Array() - - for i in range(nb_points + 1): - var angle_point = deg_to_rad(angle_from + i * (angle_to-angle_from) / nb_points - 90) - points_arc.push_back(center + Vector2(cos(angle_point), sin(angle_point)) * radius) + extends Node2D - for index_point in range(nb_points): - draw_line(points_arc[index_point], points_arc[index_point + 1], color) + var coords_head : Array = [ + [ 22.952, 83.271 ], [ 28.385, 98.623 ], + [ 53.168, 107.647 ], [ 72.998, 107.647 ], + [ 99.546, 98.623 ], [ 105.048, 83.271 ], + [ 105.029, 55.237 ], [ 110.740, 47.082 ], + [ 102.364, 36.104 ], [ 94.050, 40.940 ], + [ 85.189, 34.445 ], [ 85.963, 24.194 ], + [ 73.507, 19.930 ], [ 68.883, 28.936 ], + [ 59.118, 28.936 ], [ 54.494, 19.930 ], + [ 42.039, 24.194 ], [ 42.814, 34.445 ], + [ 33.951, 40.940 ], [ 25.637, 36.104 ], + [ 17.262, 47.082 ], [ 22.973, 55.237 ] + ] .. code-tab:: csharp - public void DrawCircleArc(Vector2 center, float radius, float angleFrom, float angleTo, Color color) - { - int nbPoints = 32; - var pointsArc = new Vector2[nbPoints + 1]; + using Godot; - for (int i = 0; i <= nbPoints; i++) + public partial class MyNode2D : Node2D + { + private float[,] _coordsHead = { - float anglePoint = Mathf.DegToRad(angleFrom + i * (angleTo - angleFrom) / nbPoints - 90f); - pointsArc[i] = center + new Vector2(Mathf.Cos(anglePoint), Mathf.Sin(anglePoint)) * radius; - } + { 22.952f, 83.271f }, { 28.385f, 98.623f }, + { 53.168f, 107.647f }, { 72.998f, 107.647f }, + { 99.546f, 98.623f }, { 105.048f, 83.271f }, + { 105.029f, 55.237f }, { 110.740f, 47.082f }, + { 102.364f, 36.104f }, { 94.050f, 40.940f }, + { 85.189f, 34.445f }, { 85.963f, 24.194f }, + { 73.507f, 19.930f }, { 68.883f, 28.936f }, + { 59.118f, 28.936f }, { 54.494f, 19.930f }, + { 42.039f, 24.194f }, { 42.814f, 34.445f }, + { 33.951f, 40.940f }, { 25.637f, 36.104f }, + { 17.262f, 47.082f }, { 22.973f, 55.237f } + }; + } + +This format, while compact, is not the one that Godot understands to +draw a polygon. In a different scenario we could have to load +these coordinates from a file or calculate the positions while the +application is running, so some transformation may be needed. + +To transform these coordinates into the right format, we will create a new +method ``float_array_to_Vector2Array()``. Then we will override the ``_ready()`` +function, which Godot will call only once -at the start of the execution- +to load those coordinates into a variable: + +.. tabs:: + .. code-tab:: gdscript GDScript + + var head : PackedVector2Array + + func float_array_to_Vector2Array(coords : Array) -> PackedVector2Array: + # Convert the array of floats into a PackedVector2Array. + var array : PackedVector2Array = [] + for coord in coords: + array.append(Vector2(coord[0], coord[1])) + return array - for (int i = 0; i < nbPoints - 1; i++) + func _ready(): + head = float_array_to_Vector2Array(coords_head); + + .. code-tab:: csharp + + private Vector2[] _head; + + private Vector2[] FloatArrayToVector2Array(float[,] coords) + { + // Convert the array of floats into an array of Vector2. + int size = coords.GetUpperBound(0); + Vector2[] array = new Vector2[size + 1]; + for (int i = 0; i <= size; i++) { - DrawLine(pointsArc[i], pointsArc[i + 1], color); + array[i] = new Vector2(coords[i, 0], coords[i, 1]); } + return array; + } + + public override void _Ready() + { + _head = FloatArrayToVector2Array(_coordsHead); + } + +To finally draw our first shape, we will use the method +:ref:`draw_polygon ` +and pass the points (as an array of Vector2 coordinates) and its color, +like this: + +.. tabs:: + .. code-tab:: gdscript GDScript + + func _draw(): + # We are going to paint with this color. + var godot_blue : Color = Color("478cbf") + # We pass the PackedVector2Array to draw the shape. + draw_polygon(head, [ godot_blue ]) + + .. code-tab:: csharp + + public override void _Draw() + { + // We are going to paint with this color. + Color godotBlue = new Color("478cbf"); + // We pass the array of Vector2 to draw the shape. + DrawPolygon(_head, new Color[]{ godotBlue }); } +When running it you should see something like this: + +.. image:: img/draw_godot_logo_polygon.webp + +Note the lower part of the logo looks segmented- this is because a low +amount of points were used to define that part. To simulate a smooth curve, +we could add more points to our array, or maybe use a mathematical function to +interpolate a curve and create a smooth shape from code (see +:ref:`example 2`). + +Polygons will always **connect its last defined point to its first +one** in order to have a closed shape. + +Drawing connected lines +^^^^^^^^^^^^^^^^^^^^^^^ + +Drawing a sequence of connected lines that don't close down to form a polygon +is very similar to the previous method. We will use a connected set of lines to +draw Godot's logo mouth. + +First, we will define the list of coordinates that form the mouth shape, like this: + +.. tabs:: + .. code-tab:: gdscript GDScript + + var coords_mouth = [ + [ 22.817, 81.100 ], [ 38.522, 82.740 ], + [ 39.001, 90.887 ], [ 54.465, 92.204 ], + [ 55.641, 84.260 ], [ 72.418, 84.177 ], + [ 73.629, 92.158 ], [ 88.895, 90.923 ], + [ 89.556, 82.673 ], [ 105.005, 81.100 ] + ] + + .. code-tab:: csharp + + private float[,] _coordsMouth = + { + { 22.817f, 81.100f }, { 38.522f, 82.740f }, + { 39.001f, 90.887f }, { 54.465f, 92.204f }, + { 55.641f, 84.260f }, { 72.418f, 84.177f }, + { 73.629f, 92.158f }, { 88.895f, 90.923f }, + { 89.556f, 82.673f }, { 105.005f, 81.100f } + }; -Remember the number of points our shape has to be decomposed into? We fixed this -number in the ``nb_points`` variable to a value of ``32``. Then, we initialize an empty -``PackedVector2Array``, which is simply an array of ``Vector2``\ s. - -The next step consists of computing the actual positions of these 32 points that -compose an arc. This is done in the first for-loop: we iterate over the number of -points for which we want to compute the positions, plus one to include the last point. -We first determine the angle of each point, between the starting and ending angles. - -The reason why each angle is decreased by 90° is that we will compute 2D positions -out of each angle using trigonometry (you know, cosine and sine stuff...). However, -``cos()`` and ``sin()`` use radians, not degrees. The angle of 0° (0 radian) -starts at 3 o'clock, although we want to start counting at 12 o'clock. So we decrease -each angle by 90° in order to start counting from 12 o'clock. - -The actual position of a point located on a circle at angle ``angle`` (in radians) -is given by ``Vector2(cos(angle), sin(angle))``. Since ``cos()`` and ``sin()`` return values -between -1 and 1, the position is located on a circle of radius 1. To have this -position on our support circle, which has a radius of ``radius``, we simply need to -multiply the position by ``radius``. Finally, we need to position our support circle -at the ``center`` position, which is performed by adding it to our ``Vector2`` value. -Finally, we insert the point in the ``PackedVector2Array`` which was previously defined. - -Now, we need to actually draw our points. As you can imagine, we will not simply -draw our 32 points: we need to draw everything that is between each of them. -We could have computed every point ourselves using the previous method, and drew -it one by one. But this is too complicated and inefficient (except if explicitly needed), -so we simply draw lines between each pair of points. Unless the radius of our -support circle is big, the length of each line between a pair of points will -never be long enough to see them. If that were to happen, we would simply need to -increase the number of points. - -Draw the arc on the screen -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -We now have a function that draws stuff on the screen; -it is time to call it inside the ``_draw()`` function: +We will load these coordinates into a variable and define an additional +variable with the configurable line thickness: .. tabs:: + .. code-tab:: gdscript GDScript + + var mouth : PackedVector2Array + var _mouth_width : float = 4.4 + + func _ready(): + head = float_array_to_Vector2Array(coords_head); + mouth = float_array_to_Vector2Array(coords_mouth); + + .. code-tab:: csharp + + private Vector2[] _mouth; + private float _mouthWidth = 4.4f; + + public override void _Ready() + { + _head = FloatArrayToVector2Array(_coordsHead); + _mouth = FloatArrayToVector2Array(_coordsMouth); + } +And finally we will use the method +:ref:`draw_polyline ` to actually +draw the line, like this: + +.. tabs:: .. code-tab:: gdscript GDScript func _draw(): - var center = Vector2(200, 200) - var radius = 80 - var angle_from = 75 - var angle_to = 195 - var color = Color(1.0, 0.0, 0.0) - draw_circle_arc(center, radius, angle_from, angle_to, color) + # We will use white to draw the line. + var white : Color = Color.WHITE + var godot_blue : Color = Color("478cbf") + + draw_polygon(head, [ godot_blue ]) + + # We draw the while line on top of the previous shape. + draw_polyline(mouth, white, _mouth_width) + .. code-tab:: csharp public override void _Draw() { - var center = new Vector2(200, 200); - float radius = 80; - float angleFrom = 75; - float angleTo = 195; - var color = new Color(1, 0, 0); - DrawCircleArc(center, radius, angleFrom, angleTo, color); + // We will use white to draw the line. + Color white = Colors.White; + Color godotBlue = new Color("478cbf"); + + DrawPolygon(_head, new Color[]{ godotBlue }); + + // We draw the while line on top of the previous shape. + DrawPolyline(_mouth, white, mouthWidth); } -Result: +You should get the following output: + +.. image:: img/draw_godot_logo_polyline.webp -.. image:: img/result_drawarc.png +Unlike ``draw_polygon()``, polylines can only have a single unique color +for all its points (the second argument). This method has 2 additional +arguments: the width of the line (which is as small as possible by default) +and enabling or disabling the anti-aliasing (it is disabled by default). -Arc polygon function -^^^^^^^^^^^^^^^^^^^^ +The order of the ``_draw`` calls is important- like with the Node positions on +the tree hierarchy, the different shapes will be drawn from top to bottom, +resulting in the latest shapes hiding earlier ones if they overlap. In this +case we want the mouth drawn over the head, so we put it afterwards. -We can take this a step further and not only write a function that draws the plain -portion of the disc defined by the arc, but also its shape. The method is exactly -the same as before, except that we draw a polygon instead of lines: +Notice how we can define colors in different ways, either with a hexadecimal +code or a predefined color name. Check the class :ref:`Color ` for other +constants and ways to define Colors. + +Drawing circles +^^^^^^^^^^^^^^^ + +To create the eyes, we are going to add 4 additional calls to draw the eye +shapes, in different sizes, colors and positions. + +To draw a circle, you position it based on its center using the +:ref:`draw_circle ` method. The first +parameter is a :ref:`Vector2` with the coordinates of its center, the second is +its radius, and the third is its color: .. tabs:: .. code-tab:: gdscript GDScript - func draw_circle_arc_poly(center, radius, angle_from, angle_to, color): - var nb_points = 32 - var points_arc = PackedVector2Array() - points_arc.push_back(center) - var colors = PackedColorArray([color]) + func _draw(): + var white : Color = Color.WHITE + var godot_blue : Color = Color("478cbf") + var grey : Color = Color("414042") + + draw_polygon(head, [ godot_blue ]) + draw_polyline(mouth, white, _mouth_width) - for i in range(nb_points + 1): - var angle_point = deg_to_rad(angle_from + i * (angle_to - angle_from) / nb_points - 90) - points_arc.push_back(center + Vector2(cos(angle_point), sin(angle_point)) * radius) - draw_polygon(points_arc, colors) + # Four circles for the 2 eyes: 2 white, 2 grey. + draw_circle(Vector2(42.479, 65.4825), 9.3905, white) + draw_circle(Vector2(85.524, 65.4825), 9.3905, white) + draw_circle(Vector2(43.423, 65.92), 6.246, grey) + draw_circle(Vector2(84.626, 66.008), 6.246, grey) .. code-tab:: csharp - public void DrawCircleArcPoly(Vector2 center, float radius, float angleFrom, float angleTo, Color color) + + public override void _Draw() { - int nbPoints = 32; - var pointsArc = new Vector2[nbPoints + 2]; - pointsArc[0] = center; - var colors = new Color[] { color }; + Color white = Colors.White; + Color godotBlue = new Color("478cbf"); + Color grey = new Color("414042"); + + DrawPolygon(_head, new Color[]{ godotBlue }); + DrawPolyline(_mouth, white, _mouthWidth); + + // Four circles for the 2 eyes: 2 white, 2 grey. + DrawCircle(new Vector2(42.479f, 65.4825f), 9.3905f, white); + DrawCircle(new Vector2(85.524f, 65.4825f), 9.3905f, white); + DrawCircle(new Vector2(43.423f, 65.92f), 6.246f, grey); + DrawCircle(new Vector2(84.626f, 66.008f), 6.246f, grey); + } - for (int i = 0; i <= nbPoints; i++) - { - float anglePoint = Mathf.DegToRad(angleFrom + i * (angleTo - angleFrom) / nbPoints - 90); - pointsArc[i + 1] = center + new Vector2(Mathf.Cos(anglePoint), Mathf.Sin(anglePoint)) * radius; - } +When executing it, you should have something like this: + +.. image:: img/draw_godot_logo_circle.webp + + +For partial, unfilled arcs (portions of a circle shape between certain +arbitrary angles), you can use the method +:ref:`draw_arc `. + +Drawing lines +^^^^^^^^^^^^^ + +To draw the final shape (the nose) we will use a line to approximate it. + +:ref:`draw_line ` can be used to draw +a single segment by providing its start and end coordinates as arguments, +like this: + +.. tabs:: + .. code-tab:: gdscript GDScript + + func _draw(): + var white : Color = Color.WHITE + var godot_blue : Color = Color("478cbf") + var grey : Color = Color("414042") + + draw_polygon(head, [ godot_blue ]) + draw_polyline(mouth, white, _mouth_width) + draw_circle(Vector2(42.479, 65.4825), 9.3905, white) + draw_circle(Vector2(85.524, 65.4825), 9.3905, white) + draw_circle(Vector2(43.423, 65.92), 6.246, grey) + draw_circle(Vector2(84.626, 66.008), 6.246, grey) - DrawPolygon(pointsArc, colors); + # Draw a short but thick white vertical line for the nose. + draw_line(Vector2(64.273, 60.564), Vector2(64.273, 74.349), white, 5.8) + + .. code-tab:: csharp + + public override void _Draw() + { + Color white = Colors.White; + Color godotBlue = new Color("478cbf"); + Color grey = new Color("414042"); + + DrawPolygon(_head, new Color[]{ godotBlue }); + DrawPolyline(_mouth, white, _mouthWidth); + DrawCircle(new Vector2(42.479f, 65.4825f), 9.3905f, white); + DrawCircle(new Vector2(85.524f, 65.4825f), 9.3905f, white); + DrawCircle(new Vector2(43.423f, 65.92f), 6.246f, grey); + DrawCircle(new Vector2(84.626f, 66.008f), 6.246f, grey); + + // Draw a short but thick white vertical line for the nose. + DrawLine(new Vector2(64.273f, 60.564f), new Vector2(64.273f, 74.349f), + white, 5.8f); } +You should now be able to see the following shape on screen: + +.. image:: img/draw_godot_logo_line.webp -.. image:: img/result_drawarc_poly.png +Note that if multiple unconnected lines are going to be drawn at the same time, +you may get additional performance by drawing all of them in a single call, using +the :ref:`draw_multiline ` method. -Dynamic custom drawing -^^^^^^^^^^^^^^^^^^^^^^ +Drawing text +^^^^^^^^^^^^ -All right, we are now able to draw custom stuff on the screen. However, it is static; -let's make this shape turn around the center. The solution to do this is simply -to change the angle_from and angle_to values over time. For our example, -we will simply increment them by 50. This increment value has to remain -constant or else the rotation speed will change accordingly. +While using the :ref:`Label ` Node is the most common way to add +text to your application, the low level `_draw` function includes functionality +to add text to your custom Node drawing. We will use it to add the name "GODOT" +under the robot head. -First, we have to make both angle_from and angle_to variables global at the top -of our script. Also note that you can store them in other nodes and access them -using ``get_node()``. +We will use the :ref:`draw_string ` method +to do it, like this: .. tabs:: .. code-tab:: gdscript GDScript - extends Node2D + var default_font : Font = ThemeDB.fallback_font; - var rotation_angle = 50 - var angle_from = 75 - var angle_to = 195 + func _draw(): + var white : Color = Color.WHITE + var godot_blue : Color = Color("478cbf") + var grey : Color = Color("414042") + + draw_polygon(head, [ godot_blue ]) + draw_polyline(mouth, white, _mouth_width) + draw_circle(Vector2(42.479, 65.4825), 9.3905, white) + draw_circle(Vector2(85.524, 65.4825), 9.3905, white) + draw_circle(Vector2(43.423, 65.92), 6.246, grey) + draw_circle(Vector2(84.626, 66.008), 6.246, grey) + draw_line(Vector2(64.273, 60.564), Vector2(64.273, 74.349), white, 5.8) + + # Draw GODOT text below the logo with the default font, size 22. + draw_string(default_font, Vector2(20, 130), "GODOT", + HORIZONTAL_ALIGNMENT_CENTER, 90, 22) .. code-tab:: csharp - using Godot; + private Font _defaultFont = ThemeDB.FallbackFont; - public partial class MyNode2D : Node2D + public override void _Draw() { - private float _rotationAngle = 50; - private float _angleFrom = 75; - private float _angleTo = 195; + Color white = Colors.White; + Color godotBlue = new Color("478cbf"); + Color grey = new Color("414042"); + + DrawPolygon(_head, new Color[]{ godotBlue }); + DrawPolyline(_mouth, white, mouthWidth); + DrawCircle(new Vector2(42.479f, 65.4825f), 9.3905f, white); + DrawCircle(new Vector2(85.524f, 65.4825f), 9.3905f, white); + DrawCircle(new Vector2(43.423f, 65.92f), 6.246f, grey); + DrawCircle(new Vector2(84.626f, 66.008f), 6.246f, grey); + DrawLine(new Vector2(64.273f, 60.564f), new Vector2(64.273f, 74.349f), + white, 5.8f); + + // Draw GODOT text below the logo with the default font, size 22. + DrawString(_defaultFont, new Vector2(20f, 130f), "GODOT", + HorizontalAlignment.Center, 90, 22); } -We make these values change in the _process(delta) function. +Here we first load into the defaultFont variable the configured default theme +font (a custom one can be set instead) and then we pass the following +parameters: font, position, text, horizontal alignment, width, and font size. -We also increment our angle_from and angle_to values here. However, we must not -forget to ``wrap()`` the resulting values between 0 and 360°! That is, if the angle -is 361°, then it is actually 1°. If you don't wrap these values, the script will -work correctly, but the angle values will grow bigger and bigger over time until -they reach the maximum integer value Godot can manage (``2^31 - 1``). -When this happens, Godot may crash or produce unexpected behavior. +You should see the following on your screen: -Finally, we must not forget to call the ``queue_redraw()`` function, which automatically -calls ``_draw()``. This way, you can control when you want to refresh the frame. +.. image:: img/draw_godot_logo_text.webp + +Additional parameters as well as other methods related to text and characters +can be found on the :ref:`CanvasItem ` class reference. + +.. _doc_draw_show_drawing_while_editing_example: + +Show the drawing while editing +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +While the code so far is able to draw the logo on a running window, it will +not show up on the ``2D view`` on the editor. In certain cases you would +also like to show your custom Node2D or control on the editor, to position +and scale it appropriately, like most other nodes do. + +To show the logo directly on the editor (without running it), you can use the +:ref:`@tool` annotation to request the custom drawing +of the node to also appear while editing, like this: .. tabs:: .. code-tab:: gdscript GDScript - func _process(delta): - angle_from += rotation_angle - angle_to += rotation_angle + @tool + extends Node2D - # We only wrap angles when both of them are bigger than 360. - if angle_from > 360 and angle_to > 360: - angle_from = wrapf(angle_from, 0, 360) - angle_to = wrapf(angle_to, 0, 360) - queue_redraw() + .. code-tab:: csharp + + using Godot; + + [Tool] + public partial class MyNode2D : Node2D + +You will need to save your scene, rebuild your project (for C# only) and reload +the current scene manually at the menu option ``Scene > Reload Saved Scene`` +to refresh the current node in the ``2D`` view the first time you add or remove +the ``@tool`` annotation. + +Animation +^^^^^^^^^ + +If we wanted to make the custom shape change at runtime, we could modify the +methods called or its arguments at execution time, or apply a transform. + +For example, if we want the custom shape we just designed to rotate, we could add +the following variable and code to the ``_ready`` and ``_process`` methods: + +.. tabs:: + .. code-tab:: gdscript GDScript + + extends Node2D + + @export var rotation_speed : float = 1 # In radians per second. + + func _ready(): + rotation = 0 + ... + + func _process(delta: float): + rotation -= rotation_speed * delta .. code-tab:: csharp - public override void _Process(double delta) + [Export] + private float _rotationSpeed = 1.0f; // In radians per second. + + public override void _Ready() { - _angleFrom += _rotationAngle; - _angleTo += _rotationAngle; + Rotation = 0; + ... + } - // We only wrap angles when both of them are bigger than 360. - if (_angleFrom > 360 && _angleTo > 360) - { - _angleFrom = Mathf.Wrap(_angleFrom, 0, 360); - _angleTo = Mathf.Wrap(_angleTo, 0, 360); - } - QueueRedraw(); + public override void _Process(double delta) + { + Rotation -= _rotationSpeed * (float)delta; } +The problem with the above code is that because we have created the points +approximately on a rectangle starting from the upper left corner, the ``(0, 0)`` +coordinate and extending to the right and down, we see that the rotation is done +using the top left corner as pivot. A position transform change on the node +won't help us here, as the rotation transform is applied first. + +While we could rewrite all of the points' coordinates to be centered around +``(0, 0)``, including negative coordinates, that would be a lot of work. -Also, don't forget to modify the ``_draw()`` function to make use of these variables: +One possible way to work around this is to use the lower level +:ref:`draw_set_transform` +method to fix this issue, translating all points in the CanvasItem's own space, +and then moving it back to its original place with a regular node transform, +either in the editor or in code, like this: .. tabs:: .. code-tab:: gdscript GDScript - func _draw(): - var center = Vector2(200, 200) - var radius = 80 - var color = Color(1.0, 0.0, 0.0) - draw_circle_arc( center, radius, angle_from, angle_to, color ) + func _ready(): + rotation = 0 + position = Vector2(60, 60) + ... + + func _draw(): + draw_set_transform(Vector2(-60, -60)) + ... .. code-tab:: csharp - public override void _Draw() + public override void _Ready() { - var center = new Vector2(200, 200); - float radius = 80; - var color = new Color(1, 0, 0); + Rotation = 0; + Position = new Vector2(60, 60); + ... + } - DrawCircleArc(center, radius, _angleFrom, _angleTo, color); + public override void _Draw() + { + DrawSetTransform(new Vector2(-60.0f, -60.0f)); + ... } +This is the result, rotating around a pivot now on ``(60, 60)``: -Let's run! -It works, but the arc is rotating insanely fast! What's wrong? +.. image:: img/draw_godot_rotation.webp -The reason is that your GPU is actually displaying the frames as fast as it can. -We need to "normalize" the drawing by this speed; to achieve that, we have to make -use of the ``delta`` parameter of the ``_process()`` function. ``delta`` contains the -time elapsed between the two last rendered frames. It is generally small -(about 0.0003 seconds, but this depends on your hardware), so using ``delta`` to -control your drawing ensures that your program runs at the same speed on -everybody's hardware. +If what we wanted to animate was a property inside the ``_draw()`` call, we must remember to +call ``queue_redraw()`` to force a refresh, as otherwise it would not be updated on screen. -In our case, we simply need to multiply our ``rotation_angle`` variable by ``delta`` -in the ``_process()`` function. This way, our 2 angles will be increased by a much -smaller value, which directly depends on the rendering speed. +For example, this is how we can make the robot appear to open and close its mouth, by +changing the width of its mouth line follow a sinusoidal (:ref:`sin`) curve: .. tabs:: .. code-tab:: gdscript GDScript - func _process(delta): - angle_from += rotation_angle * delta - angle_to += rotation_angle * delta + var _mouth_width : float = 4.4 + var _max_width : float = 7 + var _t : float = 0 - # We only wrap angles when both of them are bigger than 360. - if angle_from > 360 and angle_to > 360: - angle_from = wrapf(angle_from, 0, 360) - angle_to = wrapf(angle_to, 0, 360) + func _process(delta : float): + _t += delta + _mouth_width = abs(sin(_t) * _max_width) queue_redraw() + func _draw(): + ... + draw_polyline(mouth, white, _mouth_width) + ... + .. code-tab:: csharp + private float _mouthWidth = 4.4f; + private float _maxWidth = 7f; + private float _t = 0f; + public override void _Process(double delta) { - _angleFrom += _rotationAngle * (float)delta; - _angleTo += _rotationAngle * (float)delta; + t += (float)delta; + _mouthWidth = Mathf.Abs(Mathf.Sin(_t) * _maxWidth); + QueueRedraw(); + } + + public override void _Draw() + { + ... + DrawPolyline(_mouth, white, _mouthWidth); + ... + } + +It will look somewhat like this when run: + +.. image:: img/draw_godot_mouth_animation.webp + +Please note that ``_mouth_width`` is a user defined property like any other +and it or any other used as a drawing argument can be animated using more +standard and high level methods such as a :ref:`Tween` or an +:ref:`AnimationPlayer` Node. The only difference is +that a ``queue_redraw()`` call is needed to apply those changes so they get +shown on screen. + +.. _doc_draw_custom_example_2: + +Example 2: drawing a dynamic line +--------------------------------- + +The previous example was useful to learn how to draw and modify nodes with +custom shapes and animations. This could have some advantages, such as using +exact coordinates and vectors for drawing, rather than bitmaps -which means +they will scale well when transformed on screen. In some cases, similar results +could be achieved composing higher level functionality with nodes such as +:ref:`sprites` or +:ref:`AnimatedSprites` loading SVG resources (which are +also images defined with vectors) and the +:ref:`AnimationPlayer` node. + +In other cases that will not be possible because we will not know what the +resulting graphical representation will be before running the code. Here we +will see how to draw a dynamic line whose coordinates are not known beforehand, +and are affected by the user's input. + +Drawing a straight line between 2 points +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Let's assume we want to draw a straight line between 2 points, the first one +will be fixed on the upper left corner ``(0, 0)`` and the second will be defined +by the cursor position on screen. + +We could draw a dynamic line between those 2 points like this: + +.. tabs:: + .. code-tab:: gdscript GDScript + + extends Node2D + + var point1 : Vector2 = Vector2(0, 0) + var width : int = 10 + var color : Color = Color.GREEN + + var _point2 : Vector2 - // We only wrap angles when both of them are bigger than 360. - if (_angleFrom > 360 && _angleTo > 360) + func _process(_delta): + var mouse_position = get_viewport().get_mouse_position() + if mouse_position != _point2: + _point2 = mouse_position + queue_redraw() + + func _draw(): + draw_line(point1, _point2, color, width) + + .. code-tab:: csharp + + using Godot; + using System; + + public partial class MyNode2DLine : Node2D + { + public Vector2 Point1 { get; set; } = new Vector2(0f, 0f); + public int Width { get; set; } = 10; + public Color Color { get; set; } = Colors.Green; + + private Vector2 _point2; + + public override void _Process(double delta) { - _angleFrom = Wrap(_angleFrom, 0, 360); - _angleTo = Wrap(_angleTo, 0, 360); + Vector2 mousePosition = GetViewport().GetMousePosition(); + if (mousePosition != _point2) + { + _point2 = mousePosition; + QueueRedraw(); + } + } + + public override void _Draw() + { + DrawLine(Point1, _point2, Color, Width); } - QueueRedraw(); } +In this example we obtain the position of the mouse in the default viewport +every frame with the method +:ref:`get_mouse_position `. If the +position has changed since the last draw request (a small optimization to +avoid redrawing on every frame)- we will schedule a redraw. Our ``_draw()`` +method only has one line: requesting the drawing of a green line of +width 10 pixels between the top left corner and that obtained position. -Let's run again! This time, the rotation displays fine! +The width, color, and position of the starting point can be configured with +with the corresponding properties. -Antialiased drawing -^^^^^^^^^^^^^^^^^^^ +It should look like this when run: -Godot offers method parameters in :ref:`draw_line` -to enable antialiasing, but not all custom drawing methods offer this ``antialiased`` -parameter. +.. image:: img/draw_line_between_2_points.webp -For custom drawing methods that don't provide an ``antialiased`` parameter, -you can enable 2D MSAA instead, which affects rendering in the entire viewport. -This provides high-quality antialiasing, but a higher performance cost and only -on specific elements. See :ref:`doc_2d_antialiasing` for more information. +Drawing an arc between 2 points +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Tools ------ +The above example works, but we may want to join those 2 points with a +different shape or function, other than a straight line. -Drawing your own nodes might also be desired while running them in the -editor. This can be used as a preview or visualization of some feature or -behavior. See :ref:`doc_running_code_in_the_editor` for more information. +Let's try now creating an arc (a portion of a circumference) between +both points. + +Exporting the line starting point, segments, width, color, and antialiasing will +allow us to modify those properties very easily directly from the editor +inspector panel: + +.. tabs:: + .. code-tab:: gdscript GDScript + + extends Node2D + + @export var point1 : Vector2 = Vector2(0, 0) + @export_range(1, 1000) var segments : int = 100 + @export var width : int = 10 + @export var color : Color = Color.GREEN + @export var antialiasing : bool = false + + var _point2 : Vector2 + + .. code-tab:: csharp + + using Godot; + using System; + + public partial class MyNode2DLine : Node2D + { + [Export] + public Vector2 Point1 { get; set; } = new Vector2(0f, 0f); + [Export] + public float Length { get; set; } = 350f; + [Export(PropertyHint.Range, "1,1000,")] + public int Segments { get; set; } = 100; + [Export] + public int Width { get; set; } = 10; + [Export] + public Color Color { get; set; } = Colors.Green; + [Export] + public bool AntiAliasing { get; set; } = false; + + private Vector2 _point2; + } + +.. image:: img/draw_dynamic_exported_properties.webp + +To draw the arc, we can use the method +:ref:`draw_arc`. There are many +arcs that pass through 2 points, so we will chose for this example +the semicircle that has its center in the middle point between the 2 initial +points. + +Calculating this arc will be more complex than in the case of the line: + +.. tabs:: + .. code-tab:: gdscript GDScript + + func _draw(): + # Calculate the arc parameters. + var center : Vector2 = Vector2((_point2.x - point1.x) / 2, + (_point2.y - point1.y) / 2) + var radius : float = point1.distance_to(_point2) / 2 + var start_angle : float = (_point2 - point1).angle() + var end_angle : float = (point1 - _point2).angle() + if end_angle < 0: # end_angle is likely negative, normalize it. + end_angle += TAU + + # Finally, draw the arc. + draw_arc(center, radius, start_angle, end_angle, segments, color, + width, antialiasing) + + .. code-tab:: csharp + + public override void _Draw() + { + // Calculate the arc parameters. + Vector2 center = new Vector2((_point2.X - Point1.X) / 2.0f, + (_point2.Y - Point1.Y) / 2.0f); + float radius = Point1.DistanceTo(_point2) / 2.0f; + float startAngle = (_point2 - Point1).Angle(); + float endAngle = (Point1 - _point2).Angle(); + if (endAngle < 0.0f) // endAngle is likely negative, normalize it. + { + endAngle += Mathf.Tau; + } + + // Finally, draw the arc. + DrawArc(center, radius, startAngle, endAngle, Segments, Color, + Width, AntiAliasing); + } + +The center of the semicircle will be the middle point between both points. +The radius will be half the distance between both points. +The start and end angles will be the angles of the vector from point1 +to point2 and vice-versa. +Note we had to normalize the ``end_angle`` in positive values because if +``end_angle`` is less than ``start_angle``, the arc will be drawn +counter-clockwise, which we don't want in this case (the arc would be +upside-down). + +The result should be something like this, with the arc going down and +between the points: + +.. image:: img/draw_arc_between_2_points.webp + +Feel free to play with the parameters in the inspector to obtain different +results: change the color, the width, the antialiasing, and increase the +number of segments to increase the curve smoothness, at the cost of extra +performance. diff --git a/tutorials/2d/img/draw_antialiasing_options.webp b/tutorials/2d/img/draw_antialiasing_options.webp new file mode 100644 index 000000000000..491c4757b867 Binary files /dev/null and b/tutorials/2d/img/draw_antialiasing_options.webp differ diff --git a/tutorials/2d/img/draw_arc_between_2_points.webp b/tutorials/2d/img/draw_arc_between_2_points.webp new file mode 100644 index 000000000000..80376ef540a1 Binary files /dev/null and b/tutorials/2d/img/draw_arc_between_2_points.webp differ diff --git a/tutorials/2d/img/draw_catenary_between_2_points.webp b/tutorials/2d/img/draw_catenary_between_2_points.webp new file mode 100644 index 000000000000..77d62d8a0155 Binary files /dev/null and b/tutorials/2d/img/draw_catenary_between_2_points.webp differ diff --git a/tutorials/2d/img/draw_dynamic_exported_properties.webp b/tutorials/2d/img/draw_dynamic_exported_properties.webp new file mode 100644 index 000000000000..beae0bbe6e41 Binary files /dev/null and b/tutorials/2d/img/draw_dynamic_exported_properties.webp differ diff --git a/tutorials/2d/img/draw_godot_logo_circle.webp b/tutorials/2d/img/draw_godot_logo_circle.webp new file mode 100644 index 000000000000..70fe0e087a6b Binary files /dev/null and b/tutorials/2d/img/draw_godot_logo_circle.webp differ diff --git a/tutorials/2d/img/draw_godot_logo_line.webp b/tutorials/2d/img/draw_godot_logo_line.webp new file mode 100644 index 000000000000..96b5c2c6a17a Binary files /dev/null and b/tutorials/2d/img/draw_godot_logo_line.webp differ diff --git a/tutorials/2d/img/draw_godot_logo_polygon.webp b/tutorials/2d/img/draw_godot_logo_polygon.webp new file mode 100644 index 000000000000..28a4c35bdbe5 Binary files /dev/null and b/tutorials/2d/img/draw_godot_logo_polygon.webp differ diff --git a/tutorials/2d/img/draw_godot_logo_polyline.webp b/tutorials/2d/img/draw_godot_logo_polyline.webp new file mode 100644 index 000000000000..5b15fa2cb5f8 Binary files /dev/null and b/tutorials/2d/img/draw_godot_logo_polyline.webp differ diff --git a/tutorials/2d/img/draw_godot_logo_text.webp b/tutorials/2d/img/draw_godot_logo_text.webp new file mode 100644 index 000000000000..9b76dbc0fdf6 Binary files /dev/null and b/tutorials/2d/img/draw_godot_logo_text.webp differ diff --git a/tutorials/2d/img/draw_godot_mouth_animation.webp b/tutorials/2d/img/draw_godot_mouth_animation.webp new file mode 100644 index 000000000000..259ca5bd767e Binary files /dev/null and b/tutorials/2d/img/draw_godot_mouth_animation.webp differ diff --git a/tutorials/2d/img/draw_godot_rotation.webp b/tutorials/2d/img/draw_godot_rotation.webp new file mode 100644 index 000000000000..3ae56e2a13ea Binary files /dev/null and b/tutorials/2d/img/draw_godot_rotation.webp differ diff --git a/tutorials/2d/img/draw_line_between_2_points.webp b/tutorials/2d/img/draw_line_between_2_points.webp new file mode 100644 index 000000000000..373f5943d475 Binary files /dev/null and b/tutorials/2d/img/draw_line_between_2_points.webp differ diff --git a/tutorials/2d/img/result_drawarc.png b/tutorials/2d/img/result_drawarc.png deleted file mode 100644 index 01b0a603197c..000000000000 Binary files a/tutorials/2d/img/result_drawarc.png and /dev/null differ diff --git a/tutorials/2d/img/result_drawarc_poly.png b/tutorials/2d/img/result_drawarc_poly.png deleted file mode 100644 index 9cc1d9a7b850..000000000000 Binary files a/tutorials/2d/img/result_drawarc_poly.png and /dev/null differ