【Godot4.x】UIに使えそうな2D図形ノード

今回はUIに使えそうな2D図形ノードを作ったので公開しておきます。

UIに使えそうな2D図形ノード

Godot Engine デフォルトだと、図形ノードは "ColorRect" の四角形のみです。

これはこれで便利なのですが、円や扇形や角丸四角形、アウトラインをつけるには画像を用意しなければならず(もしくは毎回 _draw()で実装する)、少し不便だったので図形を簡単に描画できるノードを作りました。

使い方

インスペクターの「Type」から描画したい図形のタイプを選び、対応するパラメータを指定していきます。

パラメータ説明

カテゴリ 項目名 説明
共通 Type 図形の種類
Circle: 正円
Ellipse: 楕円
Fill Arc: 扇形
Rect: 四角形
Round Rect: 角丸四角形
  Color
  Centered 中央揃えにするかどうか
共通 > Outline Enabled
Outline
アウトラインを
有効にするかどうか
  Outline
Width
アウトラインの幅
  Outline
Color
アウトラインの色
Circle/Ellipse Radius 半径
  Radius
Xratio
楕円にしたときの
X方向の倍率
  Radius
Yratio
楕円にしたときの
Y方向の倍率
Fill Arc Start 開始角度
  Arc 描画する角度の大きさ
  Divide 円の分割数
(描画精度)
Rect Size 四角形の大きさ
Round Rect Round
Xratio
角丸の割合(X)
  Round
Yratio
角丸の割合(Y)

ソースコード

ソースコードGitHub にアップロードしています。

ただ特殊なことはしていないので、以下のスクリプトNode2D にアタッチすると実装できます。

@tool
extends Node2D
# =========================================================
# 2D図形描画.
# =========================================================

# ---------------------------------------------------------
# const.
# ---------------------------------------------------------
## 図形の種類.
enum eType {
    CIRCLE, # 正円.
    ELLIPSE, # 楕円.
    FILL_ARC, # 塗りつぶし円弧.
    RECT, # 矩形.
    ROUND_RECT, # 角丸矩形.
}

## 円.
const TBL_CIRCLE = [eType.CIRCLE, eType.ELLIPSE, eType.FILL_ARC]
## 矩形.
const TBL_RECT = [eType.RECT, eType.ROUND_RECT]

# ---------------------------------------------------------
# export.
# ---------------------------------------------------------
## 図形の種類.
@export var type = eType.CIRCLE
## 色.
@export var color = Color.WHITE
## 中央揃え.
@export var centered = true

## アウトライン
@export_group("Outline")
## アウトラインを有効にするかどうか.
@export var enabled_outline = false
## アウトラインの線の太さ.
@export_range(0.0, 64.0) var outline_width = 3.0
## アウトラインの色.
@export var outline_color = Color.DODGER_BLUE

## ------------ circle
@export_category("Circle/Ellipse")
## 半径.
@export_range(0.0, 512.0) var radius:float = 64.0
## 縦横の割合.
@export_range(0.0, 10.0) var radius_xratio:float = 1.0
@export_range(0.0, 10.0) var radius_yratio:float = 1.0
## ------------ fill arc
@export_category("Fill Arc")
## 開始角度.
@export_range(0.0, 360.0) var start:float = 0.0
## 広さ.
@export_range(0.0, 360.0) var arc:float = 45.0
@export_range(4, 256) var divide:int = 64
## ------------ rect
@export_category("Rect")
## 幅と高さ.
@export var size = Vector2(128.0, 64.0)
## ------------ round rect
@export_category("Round Rect")
@export_range(0.0, 1.0) var round_xratio:float = 0.1
@export_range(0.0, 1.0) var round_yratio:float = 0.2

# ---------------------------------------------------------
# public function.
# ---------------------------------------------------------
## 更新 (再描画の要求をする)
func update() -> void:
    queue_redraw()

# ---------------------------------------------------------
# private function.
# ---------------------------------------------------------
func _process(_delta: float) -> void:
    update()
    
func _draw() -> void:
    call("_draw_" + eType.keys()[type])

# ---------------------------------------------------------
# private draw function.
# ---------------------------------------------------------
## 基準の座標を取得する.
func _get_base_pos() -> Vector2:
    if type in TBL_CIRCLE:
        # 円の場合.
        if centered:
            return Vector2.ZERO 
        else:
            return Vector2(radius, radius)
    else:
        # 矩形の場合.
        if centered:
            return -(size/2)
        else:
            return Vector2.ZERO

## 円の描画.
func _draw_CIRCLE() -> void:
    var base = _get_base_pos()
    draw_circle(base, radius, color)
    
    if enabled_outline:
        # アウトラインの描画.
        draw_arc(base, radius, 0.0, 2*PI, divide, outline_color, outline_width)

## 楕円の描画.
func _draw_ELLIPSE() -> void:
    # 塗りつぶし用.
    var points = PackedVector2Array()
    var colors = PackedColorArray()
    # アウトライン用.
    var points2 = PackedVector2Array()
    
    var base = _get_base_pos()
    points.append(base)
    colors.append(color)
    var rad = 0.0
    var d = 2 * PI / (divide-1)
    for i in range(divide):
        var v = base
        v.x += (radius * radius_xratio) * cos(rad)
        v.y += (radius * radius_yratio) * sin(rad)
        points.append(v)
        colors.append(color)
        points2.append(v)
        rad += d
    draw_polygon(points, colors)
    
    if enabled_outline:
        # アウトラインの描画.
        draw_polyline(points2, outline_color, outline_width)

## 塗りつぶし円弧の描画.
func _draw_FILL_ARC() -> void:
    # 塗りつぶし用.
    var points = PackedVector2Array()
    var colors = PackedColorArray()
    # アウトライン用.
    var points2 = PackedVector2Array()
    
    var base = _get_base_pos()
    points.append(base)
    colors.append(color)
    var rad = deg_to_rad(start)
    var d = deg_to_rad(arc) / (divide - 1)
    for i in range(divide):
        var v = base
        v.x += radius * cos(rad)
        v.y += radius * sin(rad)
        points.append(v)
        colors.append(color)
        points2.append(v)
        rad += d
    draw_polygon(points, colors)
    
    if enabled_outline:
        # アウトラインの描画.
        draw_polyline(points2, outline_color, outline_width)

## 矩形の描画.
func _draw_RECT() -> void:
    var pos = _get_base_pos()
    var rect = Rect2(pos, size)
    draw_rect(rect, color)
    
    if enabled_outline:
        # アウトラインの描画.
        draw_rect(rect, outline_color, false, outline_width)

## 角丸矩形の描画.
func _draw_ROUND_RECT() -> void:
    # 2c 3b 3b 3b 2d
    # 3a 1  1  1  3a
    # 2b 3b 3b 3b 2a
    var base = _get_base_pos()
    var xrate = round_xratio/2
    var yrate = round_yratio/2
    
    # アウトライン用.
    var points2 = PackedVector2Array()
    
    # 1の部分を描画.
    var pos1 = base + Vector2(size.x * xrate, size.y * yrate)
    var size1 = Vector2()
    size1.x = size.x - (size.x * (xrate*2))
    size1.y = size.y - (size.y * (yrate*2))
    var rect1 = Rect2(pos1, size1)
    #draw_rect(rect1, color) # 3で描画するので不要.
    
    # 2[a-d]の部分を描画.
    var a = pos1 + size1
    var b = pos1 + Vector2(0, size1.y)
    var c = pos1
    var d = pos1 + Vector2(size1.x, 0)
    var idx = 0
    var size2 = Vector2()
    size2.x = size.x * xrate
    size2.y = size.y * yrate
    # 90度ずつ描画.
    var rad = 0.0
    var d_rad = (2 * PI / 4) / (divide-1)
    for pos2 in [a, b, c, d]:
        var points = PackedVector2Array()
        var colors = PackedColorArray()
        points.append(pos2)
        colors.append(color)
        for i in range(divide):
            var v = pos2
            v.x += size2.x * cos(rad)
            v.y += size2.y * sin(rad)
            points.append(v)
            colors.append(color)
            points2.append(v)
            
            rad += d_rad
        draw_polygon(points, colors)
        rad -= d_rad
    
    # 3aの部分を描画.
    var pos3a = Vector2(base.x, pos1.y)
    var size3a = Vector2(size.x, size1.y)
    var rect3a = Rect2(pos3a, size3a)
    draw_rect(rect3a, color)
    
    # 3bの部分を描画.
    # 左側.
    var pos3b1 = Vector2(pos1.x, base.y)
    var size3b1 = Vector2(size1.x, size2.y)
    var rect3b1 = Rect2(pos3b1, size3b1)
    draw_rect(rect3b1, color)
    
    # 右側.
    var pos3b2 = Vector2(pos1.x, pos1.y+size1.y)
    var size3b2 = Vector2(size1.x, size2.y)
    var rect3b2 = Rect2(pos3b2, size3b2)
    draw_rect(rect3b2, color)

    if enabled_outline:
        # 最初の点をつなぐ.
        var v = points2[0]
        points2.append(v)
        # アウトラインの描画.
        draw_polyline(points2, outline_color, outline_width)

より多くの図形に対応するには

作った後で知ったのですが「2次元ディスタンスフィールド」というのがあって、中心を基準にしてシェーダーで実装すると色々な図形が描画できるようです。

こちらのページにある「2D distance functions」を見ると以下の短いシェーダーコードで角丸四角形を描画できるような感じです。

float sdRoundedBox( in vec2 p, in vec2 b, in vec4 r )
{
    r.xy = (p.x>0.0)?r.xy : r.zw;
    r.x  = (p.y>0.0)?r.x  : r.y;
    vec2 q = abs(p)-b+r.x;
    return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - r.x;
}

ただシェーダーで書くと図形の種類だけシェーダーファイルが増えて、少しだけ管理が面倒になるので、速度が問題にならなければ今回の方法が良いかもしれません。