Up to date
This page is up to date for Godot 4.2
.
If you still find outdated information, please open an issue.
2D 中的自訂繪圖¶
前言¶
Godot 有用於繪製精靈、多邊形、粒子以及各種東西的節點。在大多數情況下,這就已經足夠了,但並不總是這樣。可別因為不存在繪製*某種特定*東西的節點,而在恐懼、焦慮、憤怒……中哭泣。要知道,任何 2D 節點(不管它是繼承自 Control 還是 Node2D)都可以很輕鬆地繪製自訂命令。而且這*真的*非常容易操作。
2D 節點中的自訂繪製*非常*有用。下面是一些用例:
繪製現有節點型別無法完成的形狀或邏輯,例如帶有軌跡或特殊動態多邊形的圖像。
與節點不太相容的呈現方式,比如俄羅斯方塊的棋盤。(俄羅斯方塊的例子使用的是自訂繪製函式來繪製方塊。)
繪製大量簡單的物件。自訂繪製避免了使用大量節點的開銷,能降低記憶體佔用,並提高性能。
製作自訂的 UI 控制項,以滿足很多可用的控制項之外的特別需求。
繪製¶
新增一個腳本到任何 CanvasItem 的衍生節點,如 Control 或 Node2D。然後重載 _draw()
函式。
extends Node2D
func _draw():
# Your draw commands here
pass
public override void _Draw()
{
// Your draw commands here
}
繪製命令在 CanvasItem 的類參考中有所描述,數量很多。
更新¶
_draw()
函式只呼叫一次, 然後繪製命令被快取並記住, 因此不需要進一步呼叫.
如果因為狀態或其他方面的變化而需要重新繪製,在目前節點中呼叫 CanvasItem.update() ,觸發新的 _draw()
呼叫。
這是一個更複雜的範例,一個被修改就會重新繪製的紋理變數:
extends Node2D
@export var texture: Texture:
set = _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.
func _draw():
draw_texture(texture, Vector2())
using Godot;
public partial class MyNode2D : Node2D
{
private Texture _texture;
public Texture Texture
{
get
{
return _texture;
}
set
{
_texture = value;
QueueRedraw();
}
}
public override void _Draw()
{
DrawTexture(_texture, new Vector2());
}
}
在某些情況下, 可能需要繪製每一影格. 為此, 只需從 _process()
回呼函式呼叫 update()
, 如下所示:
extends Node2D
func _draw():
# Your draw commands here
pass
func _process(delta):
queue_redraw()
using Godot;
public partial class CustomNode2D : Node2D
{
public override void _Draw()
{
// Your draw commands here
}
public override void _Process(double delta)
{
QueueRedraw();
}
}
座標¶
繪圖 API 使用 CanvasItem 的坐標系統,不一定是像素座標。這意味著它使用在應用 CanvasItem 的變換後建立的座標空間。此外,你可以使用 draw_set_transform 或 draw_set_transform_matrix 在它上面應用自訂變換。
使用 draw_line
時,應考慮線條的寬度。如果使用的寬度是奇數,則應將位置移動 0.5
以保持線條居中,如下圖所示。
func _draw():
draw_line(Vector2(1.5, 1.0), Vector2(1.5, 4.0), Color.GREEN, 1.0)
draw_line(Vector2(4.0, 1.0), Vector2(4.0, 4.0), Color.GREEN, 2.0)
draw_line(Vector2(7.5, 1.0), Vector2(7.5, 4.0), Color.GREEN, 3.0)
public override void _Draw()
{
DrawLine(new Vector2(1.5f, 1.0f), new Vector2(1.5f, 4.0f), Colors.Green, 1.0f);
DrawLine(new Vector2(4.0f, 1.0f), new Vector2(4.0f, 4.0f), Colors.Green, 2.0f);
DrawLine(new Vector2(7.5f, 1.0f), new Vector2(7.5f, 4.0f), Colors.Green, 3.0f);
}
這同樣適用於使用 filled = false
的 draw_rect
方法。
func _draw():
draw_rect(Rect2(1.0, 1.0, 3.0, 3.0), Color.GREEN)
draw_rect(Rect2(5.5, 1.5, 2.0, 2.0), Color.GREEN, false, 1.0)
draw_rect(Rect2(9.0, 1.0, 5.0, 5.0), Color.GREEN)
draw_rect(Rect2(16.0, 2.0, 3.0, 3.0), Color.GREEN, false, 2.0)
public override void _Draw()
{
DrawRect(new Rect2(1.0f, 1.0f, 3.0f, 3.0f), Colors.Green);
DrawRect(new Rect2(5.5f, 1.5f, 2.0f, 2.0f), Colors.Green, false, 1.0f);
DrawRect(new Rect2(9.0f, 1.0f, 5.0f, 5.0f), Colors.Green);
DrawRect(new Rect2(16.0f, 2.0f, 3.0f, 3.0f), Colors.Green, false, 2.0f);
}
範例:繪製圓弧¶
我們現在將使用 Godot 引擎的自訂繪圖功能來繪製 Godot 未提供函式的內容。比如,Godot 提供了 draw_circle()
函式,它可以繪製一個完整的圓。但是,畫一個圓的一部分怎麼說?你必須編寫一個函式來執行此操作,自己繪製它。
弧函式¶
弧由其所在的圓的參數定義. 即: 中心位置和半徑. 弧本身由開始的角度和停止的角度來定義. 這些是我們必須為繪圖提供的4個參數. 我們還將提供顏色值, 因此我們可以根據需要繪製不同顏色的圓弧.
基本上, 在螢幕上繪製形狀需要將其分解為一定量首位相接的點. 你可以預見到, 點越多, 它就越平滑, 但處理開銷就越大. 一般來說, 如果你的形狀很大(或者在3D場景中靠近相機), 則需要繪製更多的點才不會看起來像是有棱角的. 相反, 如果你的形狀很小(或在3D場景裡遠離相機), 你可以減少其點數以節省處理成本. 這稱為 多層次細節(Level of Detail, LoD) . 在我們的範例中, 無論半徑如何, 我們都只使用固定數量的點.
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)
for index_point in range(nb_points):
draw_line(points_arc[index_point], points_arc[index_point + 1], color)
public void DrawCircleArc(Vector2 center, float radius, float angleFrom, float angleTo, Color color)
{
int nbPoints = 32;
var pointsArc = new Vector2[nbPoints + 1];
for (int i = 0; i <= nbPoints; i++)
{
float anglePoint = Mathf.DegToRad(angleFrom + i * (angleTo - angleFrom) / nbPoints - 90f);
pointsArc[i] = center + new Vector2(Mathf.Cos(anglePoint), Mathf.Sin(anglePoint)) * radius;
}
for (int i = 0; i < nbPoints - 1; i++)
{
DrawLine(pointsArc[i], pointsArc[i + 1], color);
}
}
還記得我們的形狀必須分解成多少點嗎?我們將「nb_points」變數中的這個數字固定為「32」。然後,我們初始化一個空的“PackedVector2Array”,它只是一個“Vector2”陣列。
下一步包括計算構成弧的這32個點的實際位置. 這是在第一個for迴圈中完成的: 我們反覆運算我們想要計算位置的點的數量, 後面+1來包括最後一個點. 我們首先確定起點和終點之間每個點的角度.
每個角度減小90°的原因是我們將使用三角函式計算每個角度的2D位置(你知道, 餘弦和正弦之類的東西......). 但是, 為了簡單, cos()
和 sin()
使用弧度, 而不是度數作為參數. 雖然我們想在12點鐘位置開始計數, 但0°(0弧度)的角度從3點鐘位置開始. 因此我們將每個角度減小90°, 以便從12點位置開始計數.
以角度 angle
(單位是弧度)位於圓上的點的實際位置由 Vector2(cos(angle), sin(angle))
給出. 由於 cos()
和 sin()
返回介於-1和1之間的值, 因此位置位於半徑為1的圓上. 要將此位置放在我們的半徑為 radius
的輔助圓上, 我們只需要將那個位置乘以 radius
. 最後, 我們需要將我們的輔助圓定位在 center
位置, 這是通過將其與我們的 Vector2
相加來實作的. 最後, 我們在之前定義的 PoolVector2Array
中插入這個點.
現在, 我們需要實際繪製我們的點. 你可以想像, 我們不會簡單地畫出我們的32個點: 我們需要繪製每一點之間的所有內容. 我們可以使用前面的方法自己計算每個點, 然後逐個繪製. 但這太複雜和低效了(除非確實需要). 因此, 我們只需在每對點之間繪製線條. 除非我們的輔助圓的半徑很大, 否則一對點之間每條線的長度永遠不會長到足以看到它們. 如果發生這種情況, 我們只需要增加點的個數就可以了.
在螢幕上繪製弧形¶
我們現在有一個在螢幕上繪製內容的函式; 是時候在 _draw()
函式中呼叫它了:
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)
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);
}
結果:
弧多邊形函式¶
我們可以更進一步, 不僅僅繪製一個由弧定義的扇形的邊緣, 還可以繪製其形體. 該方法與以前完全相同, 只是我們繪製的是多邊形而不是線條:
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])
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)
public void DrawCircleArcPoly(Vector2 center, float radius, float angleFrom, float angleTo, Color color)
{
int nbPoints = 32;
var pointsArc = new Vector2[nbPoints + 2];
pointsArc[0] = center;
var colors = new Color[] { color };
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;
}
DrawPolygon(pointsArc, colors);
}
動態自訂繪圖¶
好吧, 我們現在能夠在螢幕上繪製自訂內容. 然而, 它是靜態的; 我們讓這個形狀圍繞中心轉動吧. 這樣做的方法就是隨著時間的推移改變angle_from和angle_to值. 對於我們的範例, 我們將簡單地將它們遞增50. 此差異量值必須保持不變, 否則旋轉速度將相應地改變.
首先, 我們必須在我們的angle_from和angle_to變數變成全域變數, 放在腳本頂部. 另請注意, 您可以將它們儲存在其他節點中並使用 get_node()
存取它們.
extends Node2D
var rotation_angle = 50
var angle_from = 75
var angle_to = 195
using Godot;
public partial class MyNode2D : Node2D
{
private float _rotationAngle = 50;
private float _angleFrom = 75;
private float _angleTo = 195;
}
我們在_process(delta)函式中更改這些值.
我們也在這裡增加angle_from和angle_to值. 但是, 我們不能忘記將結果 wrap()
在0到360°之間! 也就是說, 如果角度是361°, 那麼它實際上是1°. 如果您不包裝這些值, 腳本將正常工作, 但角度值將隨著時間的推移變得越來越大, 直到它們達到Godot可以管理的最大整數值(2^31 - 1
). 當發生這種情況時,Godot可能會當機或產生意外行為.
最後, 我們一定不要忘記呼叫 queue_redraw()
函式, 它會自動呼叫 _draw()
。這樣, 你就可以控制何時去更新這一影格。
func _process(delta):
angle_from += rotation_angle
angle_to += rotation_angle
# 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()
public override void _Process(double delta)
{
_angleFrom += _rotationAngle;
_angleTo += _rotationAngle;
// 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();
}
另外, 不要忘記修改 _draw()
函式來使用這些變數:
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 )
public override void _Draw()
{
var center = new Vector2(200, 200);
float radius = 80;
var color = new Color(1, 0, 0);
DrawCircleArc(center, radius, _angleFrom, _angleTo, color);
}
我們運作吧! 它工作正常, 但弧線旋轉快得瘋掉了! 怎麼了?
原因是你的GPU實際上正在盡可能快地顯示影格. 我們需要以這個速度為基準 "標準化" 繪圖的速度. 為了實作這個效果, 我們必須使用 _process()
函式的 delta
參數. delta
包含最後兩個渲染影格之間經過的時間. 它通常很小(約0.0003秒, 但這取決於你的硬體). 因此, 使用 delta
來控制繪圖可確保程式在每個人的硬體上以相同的速度運作.
在我們的範例中, 我們只需要在 _process()
函式中將 rotation_angle
變數乘以 delta
. 這樣, 我們的2個角度會以一個小得多的值遞增, 值的大小直接取決於渲染速度.
func _process(delta):
angle_from += rotation_angle * delta
angle_to += rotation_angle * delta
# 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()
public override void _Process(double delta)
{
_angleFrom += _rotationAngle * (float)delta;
_angleTo += _rotationAngle * (float)delta;
// We only wrap angles when both of them are bigger than 360.
if (_angleFrom > 360 && _angleTo > 360)
{
_angleFrom = Wrap(_angleFrom, 0, 360);
_angleTo = Wrap(_angleTo, 0, 360);
}
QueueRedraw();
}
讓我們再運作一次! 這次, 旋轉顯示正常!
抗鋸齒:¶
Godot在 draw_line<class_CanvasItem_method_draw_line>`中提供方法參數來啟用抗鋸齒功能,但並非所有自訂繪圖方法都提供這個 ``抗鋸齒(antialiased)` 參數。
對於不提供 抗鋸齒(antialiased)
參數的自訂繪圖方法,你可以啟用 2D MSAA,這會影響整個視口的渲染。這個功能(2D MSAA)提供了高品質的抗鋸齒,但性能成本更高,而且只適用於特定元素。參見 抗鋸齒: 以瞭解更多資訊。
工具¶
在編輯器中運作節點時,可能也會用到繪圖。可以用於某些功能或行為的預覽或視覺化。詳情請參閱 在編輯器中運作程式碼。