2D 中的自定义绘图

前言

Godot 有用于绘制精灵、多边形、粒子以及各种东西的节点。在大多数情况下,这就已经足够了。如果没有节点可以绘制你需要的特定内容,你可以用任何 2D 节点(例如,基于 ControlNode2D )绘制自定义命令。

2D 节点中的自定义绘制非常有用。下面是一些用例:

  • 绘制现有节点类型无法完成的形状或逻辑,例如带有轨迹或特殊动态多边形的图像。

  • 与节点不太兼容的呈现方式,比如俄罗斯方块的棋盘。(俄罗斯方块的例子使用的是自定义绘制函数来绘制方块。)

  • 绘制大量简单的对象。自定义绘制避免了使用大量节点的开销,能降低内存占用,并提高性能。

  • 制作自定义的 UI 控件,以满足很多可用的控件之外的特别需求。

绘制

添加一个脚本到任何 CanvasItem 的派生节点,如 ControlNode2D。然后重载 _draw() 函数。

GDScriptC#

  1. extends Node2D
  2. func _draw():
  3. # Your draw commands here
  4. pass
  1. public override void _Draw()
  2. {
  3. // Your draw commands here
  4. }

绘制命令在 CanvasItem 的类参考中有所描述,数量很多。

更新

_draw() 函数只调用一次, 然后绘制命令被缓存并记住, 因此不需要进一步调用.

如果因为状态或其他方面的变化而需要重新绘制,在当前节点中调用 CanvasItem.queue_redraw() ,触发新的 _draw() 调用。

这是一个更复杂的示例,一个被修改就会重新绘制的纹理变量:

GDScriptC#

  1. extends Node2D
  2. @export var texture: Texture:
  3. set = _set_texture
  4. func _set_texture(value):
  5. # If the texture variable is modified externally,
  6. # this callback is called.
  7. texture = value # Texture was changed.
  8. queue_redraw() # Trigger a redraw of the node.
  9. func _draw():
  10. draw_texture(texture, Vector2())
  1. using Godot;
  2. public partial class MyNode2D : Node2D
  3. {
  4. private Texture _texture;
  5. public Texture Texture
  6. {
  7. get
  8. {
  9. return _texture;
  10. }
  11. set
  12. {
  13. _texture = value;
  14. QueueRedraw();
  15. }
  16. }
  17. public override void _Draw()
  18. {
  19. DrawTexture(_texture, new Vector2());
  20. }
  21. }

在某些情况下,可能需要绘制每一帧。 为此,从 _process() 回调中调用 queue_redraw() ,如下所示:

GDScriptC#

  1. extends Node2D
  2. func _draw():
  3. # Your draw commands here
  4. pass
  5. func _process(delta):
  6. queue_redraw()
  1. using Godot;
  2. public partial class CustomNode2D : Node2D
  3. {
  4. public override void _Draw()
  5. {
  6. // Your draw commands here
  7. }
  8. public override void _Process(double delta)
  9. {
  10. QueueRedraw();
  11. }
  12. }

坐标

绘图 API 使用 CanvasItem 的坐标系,不一定是像素坐标。这意味着它使用在应用 CanvasItem 的变换后创建的坐标空间。此外,你可以使用 draw_set_transformdraw_set_transform_matrix 在它上面应用自定义变换。

使用 draw_line 时,应考虑线条的宽度。如果使用的宽度是奇数,则应将位置移动 0.5 以保持线条居中,如下图所示。

../../_images/draw_line.png

GDScriptC#

  1. func _draw():
  2. draw_line(Vector2(1.5, 1.0), Vector2(1.5, 4.0), Color.GREEN, 1.0)
  3. draw_line(Vector2(4.0, 1.0), Vector2(4.0, 4.0), Color.GREEN, 2.0)
  4. draw_line(Vector2(7.5, 1.0), Vector2(7.5, 4.0), Color.GREEN, 3.0)
  1. public override void _Draw()
  2. {
  3. DrawLine(new Vector2(1.5f, 1.0f), new Vector2(1.5f, 4.0f), Colors.Green, 1.0f);
  4. DrawLine(new Vector2(4.0f, 1.0f), new Vector2(4.0f, 4.0f), Colors.Green, 2.0f);
  5. DrawLine(new Vector2(7.5f, 1.0f), new Vector2(7.5f, 4.0f), Colors.Green, 3.0f);
  6. }

这同样适用于使用 filled = falsedraw_rect 方法。

../../_images/draw_rect.png

GDScriptC#

  1. func _draw():
  2. draw_rect(Rect2(1.0, 1.0, 3.0, 3.0), Color.GREEN)
  3. draw_rect(Rect2(5.5, 1.5, 2.0, 2.0), Color.GREEN, false, 1.0)
  4. draw_rect(Rect2(9.0, 1.0, 5.0, 5.0), Color.GREEN)
  5. draw_rect(Rect2(16.0, 2.0, 3.0, 3.0), Color.GREEN, false, 2.0)
  1. public override void _Draw()
  2. {
  3. DrawRect(new Rect2(1.0f, 1.0f, 3.0f, 3.0f), Colors.Green);
  4. DrawRect(new Rect2(5.5f, 1.5f, 2.0f, 2.0f), Colors.Green, false, 1.0f);
  5. DrawRect(new Rect2(9.0f, 1.0f, 5.0f, 5.0f), Colors.Green);
  6. DrawRect(new Rect2(16.0f, 2.0f, 3.0f, 3.0f), Colors.Green, false, 2.0f);
  7. }

示例:绘制圆弧

我们现在将使用 Godot 引擎的自定义绘图功能来绘制 Godot 未提供函数的内容。比如,Godot 提供了 draw_circle() 函数,它可以绘制一个完整的圆。但是,画一个圆的一部分怎么说?你必须编写一个函数来执行此操作,自己绘制它。

弧函数

弧由其所在的圆的参数定义。即中心位置和半径。弧本身由开始的角度和停止的角度来定义。这些是我们必须为绘图提供的4个参数。我们还将提供颜色值,因此我们可以根据需要绘制不同颜色的圆弧。

基本上,在屏幕上绘制形状需要将其分解为一定量首位相接的点。你可以预见到,点越多,它就越平滑,但处理开销就越大。一般来说,如果你的形状很大(或者在 3D 场景中靠近相机),则需要绘制更多的点才不会看起来像是有棱角的。相反,如果你的形状很小(或在 3D 场景里远离相机),你可以减少其点数以节省处理成本。这称为 细节层次 (LOD) 。在我们的示例中,无论半径如何,我们都只使用固定数量的点。

GDScriptC#

  1. func draw_circle_arc(center, radius, angle_from, angle_to, color):
  2. var nb_points = 32
  3. var points_arc = PackedVector2Array()
  4. for i in range(nb_points + 1):
  5. var angle_point = deg_to_rad(angle_from + i * (angle_to-angle_from) / nb_points - 90)
  6. points_arc.push_back(center + Vector2(cos(angle_point), sin(angle_point)) * radius)
  7. for index_point in range(nb_points):
  8. draw_line(points_arc[index_point], points_arc[index_point + 1], color)
  1. public void DrawCircleArc(Vector2 center, float radius, float angleFrom, float angleTo, Color color)
  2. {
  3. int nbPoints = 32;
  4. var pointsArc = new Vector2[nbPoints + 1];
  5. for (int i = 0; i <= nbPoints; i++)
  6. {
  7. float anglePoint = Mathf.DegToRad(angleFrom + i * (angleTo - angleFrom) / nbPoints - 90f);
  8. pointsArc[i] = center + new Vector2(Mathf.Cos(anglePoint), Mathf.Sin(anglePoint)) * radius;
  9. }
  10. for (int i = 0; i < nbPoints - 1; i++)
  11. {
  12. DrawLine(pointsArc[i], pointsArc[i + 1], color);
  13. }
  14. }

还记得我们的形状必须分解成多少个点吗?我们将 nb_points 变量中的这个数字修改为 32 。然后,我们初始化一个空的 PackedVector2Array ,它就是一个 Vector2 数组。

下一步包括计算构成弧的这32个点的实际位置. 这是在第一个for循环中完成的: 我们迭代我们想要计算位置的点的数量, 后面+1来包括最后一个点. 我们首先确定起点和终点之间每个点的角度.

每个角度减少 90° 的原因是我们将使用三角函数(你懂的,余弦和正弦之类的东西……)计算每个角度的 2D 位置。但是, cos()sin() 使用弧度,而不是角度。 0°(0 弧度)的角度从 3 点钟位置开始,尽管我们想从 12 点钟位置开始。因此,我们将每个角度减少 90°,以便从 12 点钟开始计数。

以角度 angle (单位是弧度)位于圆上的点的实际位置由 Vector2(cos(angle), sin(angle)) 给出。由于 cos()sin() 返回介于 -1 和 1 之间的值,因此位置位于半径为 1 的圆上。要将此位置放在我们的半径为 radius 的辅助圆上,我们只需要将那个位置乘以 radius 。最后,我们需要将我们的辅助圆定位在 center 位置,这是通过将其与我们的 Vector2 相加来实现的。最后,我们在之前定义的 PackedVector2Array 中插入这个点。

现在, 我们需要实际绘制我们的点. 你可以想象, 我们不会简单地画出我们的32个点: 我们需要绘制每一点之间的所有内容. 我们可以使用前面的方法自己计算每个点, 然后逐个绘制. 但这太复杂和低效了(除非确实需要). 因此, 我们只需在每对点之间绘制线条. 除非我们的辅助圆的半径很大, 否则一对点之间每条线的长度永远不会长到足以看到它们. 如果发生这种情况, 我们只需要增加点的个数就可以了.

在屏幕上绘制弧形

我们现在有一个在屏幕上绘制内容的函数; 是时候在 _draw() 函数中调用它了:

GDScriptC#

  1. func _draw():
  2. var center = Vector2(200, 200)
  3. var radius = 80
  4. var angle_from = 75
  5. var angle_to = 195
  6. var color = Color(1.0, 0.0, 0.0)
  7. draw_circle_arc(center, radius, angle_from, angle_to, color)
  1. public override void _Draw()
  2. {
  3. var center = new Vector2(200, 200);
  4. float radius = 80;
  5. float angleFrom = 75;
  6. float angleTo = 195;
  7. var color = new Color(1, 0, 0);
  8. DrawCircleArc(center, radius, angleFrom, angleTo, color);
  9. }

结果:

../../_images/result_drawarc.png

弧多边形函数

我们可以更进一步, 不仅仅绘制一个由弧定义的扇形的边缘, 还可以绘制其形体. 该方法与以前完全相同, 只是我们绘制的是多边形而不是线条:

GDScriptC#

  1. func draw_circle_arc_poly(center, radius, angle_from, angle_to, color):
  2. var nb_points = 32
  3. var points_arc = PackedVector2Array()
  4. points_arc.push_back(center)
  5. var colors = PackedColorArray([color])
  6. for i in range(nb_points + 1):
  7. var angle_point = deg_to_rad(angle_from + i * (angle_to - angle_from) / nb_points - 90)
  8. points_arc.push_back(center + Vector2(cos(angle_point), sin(angle_point)) * radius)
  9. draw_polygon(points_arc, colors)
  1. public void DrawCircleArcPoly(Vector2 center, float radius, float angleFrom, float angleTo, Color color)
  2. {
  3. int nbPoints = 32;
  4. var pointsArc = new Vector2[nbPoints + 2];
  5. pointsArc[0] = center;
  6. var colors = new Color[] { color };
  7. for (int i = 0; i <= nbPoints; i++)
  8. {
  9. float anglePoint = Mathf.DegToRad(angleFrom + i * (angleTo - angleFrom) / nbPoints - 90);
  10. pointsArc[i + 1] = center + new Vector2(Mathf.Cos(anglePoint), Mathf.Sin(anglePoint)) * radius;
  11. }
  12. DrawPolygon(pointsArc, colors);
  13. }

../../_images/result_drawarc_poly.png

动态自定义绘图

好吧, 我们现在能够在屏幕上绘制自定义内容. 然而, 它是静态的; 我们让这个形状围绕中心转动吧. 这样做的方法就是随着时间的推移改变angle_from和angle_to值. 对于我们的示例, 我们将简单地将它们递增50. 此增量值必须保持不变, 否则旋转速度将相应地改变.

首先, 我们必须在我们的angle_from和angle_to变量变成全局变量, 放在脚本顶部. 另请注意, 你可以将它们存储在其他节点中并使用 get_node() 访问它们.

GDScriptC#

  1. extends Node2D
  2. var rotation_angle = 50
  3. var angle_from = 75
  4. var angle_to = 195
  1. using Godot;
  2. public partial class MyNode2D : Node2D
  3. {
  4. private float _rotationAngle = 50;
  5. private float _angleFrom = 75;
  6. private float _angleTo = 195;
  7. }

我们在_process(delta)函数中更改这些值.

我们也在这里增加angle_from和angle_to值. 但是, 我们不能忘记将结果 wrap() 在0到360°之间! 也就是说, 如果角度是361°, 那么它实际上是1°. 如果你不包装这些值, 脚本将正常工作, 但角度值将随着时间的推移变得越来越大, 直到它们达到Godot可以管理的最大整数值(2^31 - 1). 当发生这种情况时,Godot可能会崩溃或产生意外行为.

最后, 我们一定不要忘记调用 queue_redraw() 函数, 它会自动调用 _draw() 。这样, 你就可以控制何时去刷新这一帧。

GDScriptC#

  1. func _process(delta):
  2. angle_from += rotation_angle
  3. angle_to += rotation_angle
  4. # We only wrap angles when both of them are bigger than 360.
  5. if angle_from > 360 and angle_to > 360:
  6. angle_from = wrapf(angle_from, 0, 360)
  7. angle_to = wrapf(angle_to, 0, 360)
  8. queue_redraw()
  1. public override void _Process(double delta)
  2. {
  3. _angleFrom += _rotationAngle;
  4. _angleTo += _rotationAngle;
  5. // We only wrap angles when both of them are bigger than 360.
  6. if (_angleFrom > 360 && _angleTo > 360)
  7. {
  8. _angleFrom = Mathf.Wrap(_angleFrom, 0, 360);
  9. _angleTo = Mathf.Wrap(_angleTo, 0, 360);
  10. }
  11. QueueRedraw();
  12. }

另外, 不要忘记修改 _draw() 函数来使用这些变量:

GDScriptC#

  1. func _draw():
  2. var center = Vector2(200, 200)
  3. var radius = 80
  4. var color = Color(1.0, 0.0, 0.0)
  5. draw_circle_arc( center, radius, angle_from, angle_to, color )
  1. public override void _Draw()
  2. {
  3. var center = new Vector2(200, 200);
  4. float radius = 80;
  5. var color = new Color(1, 0, 0);
  6. DrawCircleArc(center, radius, _angleFrom, _angleTo, color);
  7. }

我们运行吧! 它工作正常, 但弧线旋转快得疯掉了! 怎么了?

原因是你的 GPU 实际上正在尽可能快地显示帧。我们需要以这个速度为基准 “标准化” 绘图的速度。为了实现这个效果,我们必须使用 _process() 函数的 delta 参数。delta 包含最后两个渲染帧之间经过的时间。它通常很小(约 0.0003 秒,但这取决于你的硬件)。因此,使用 delta 来控制绘图可确保程序在每个人的硬件上以相同的速度运行。

在我们的示例中,我们只需要在 _process() 函数中将 rotation_angle 变量乘以 delta。这样,我们的 2 个角度会以一个小得多的值递增,值的大小直接取决于渲染速度。

GDScriptC#

  1. func _process(delta):
  2. angle_from += rotation_angle * delta
  3. angle_to += rotation_angle * delta
  4. # We only wrap angles when both of them are bigger than 360.
  5. if angle_from > 360 and angle_to > 360:
  6. angle_from = wrapf(angle_from, 0, 360)
  7. angle_to = wrapf(angle_to, 0, 360)
  8. queue_redraw()
  1. public override void _Process(double delta)
  2. {
  3. _angleFrom += _rotationAngle * (float)delta;
  4. _angleTo += _rotationAngle * (float)delta;
  5. // We only wrap angles when both of them are bigger than 360.
  6. if (_angleFrom > 360 && _angleTo > 360)
  7. {
  8. _angleFrom = Wrap(_angleFrom, 0, 360);
  9. _angleTo = Wrap(_angleTo, 0, 360);
  10. }
  11. QueueRedraw();
  12. }

让我们再运行一次! 这次, 旋转显示正常!

抗锯齿绘图

Godot 在 draw_line 方法中提供参数来启用抗锯齿功能,但并非所有自定义绘图方法都提供这个 抗锯齿(antialiased) 参数。

对于不提供 antialiased 参数的自定义绘图方法,你可以启用 2D MSAA,这会影响整个视口的渲染。这个功能(2D MSAA)提供了高质量的抗锯齿,但性能成本更高,而且只适用于特定元素。详情见 2D 抗锯齿

工具

在编辑器中运行节点时,可能也会用到绘图。可以用于某些特性或行为的预览或可视化。详情请参阅 在编辑器中运行代码

Previous Next


© 版权所有 2014-present Juan Linietsky, Ariel Manzur and the Godot community (CC BY 3.0). Revision b1c660f7.

Built with Sphinx using a theme provided by Read the Docs.