Власне малювання в 2D

Вступ

Godot має вузли для малювання спрайтів, багатокутників, частинок і всіляких речей. Для більшості випадків цього достатньо; але не завжди. Перш ніж плакати від безсилля, страху і люті, тому що вузла для малювання чогось конкретного не існує ... було б добре знати, що можна легко змусити будь-який вузол 2D (на основі Control, або Node2D) малювати власні команди. Це насправді дуже легко зробити.

Власне малювання в 2D вузлах дійсно корисне. Ось деякі випадки використання:

  • Малювання фігур, або логіки, які не можуть зробити існуючі вузли, наприклад, зображення зі слідами, або спеціальний анімований багатокутник.

  • Візуалізації, несумісні з вузлами, наприклад, дошка тетріса. (Для прикладу тетріс використовує спеціальну функцію малювання для малювання блоків.)

  • Малювання великої кількості простих об'єктів. Власне малюнок дозволяє уникнути використання великої кількості вузлів, можливо, знижуючи використання пам'яті та покращуючи продуктивність.

  • Створення власного елемента керування інтерфейсом користувача. Доступно багато елементів керування, але коли у вас є незвичайні потреби, вам, ймовірно, знадобиться власний контроль.

Малюнок

Додайте скрипт до будь-якого похідного вузла CanvasItem, наприклад Control, або Node2D. Потім перевизначте функцію _draw().

extends Node2D

func _draw():
    # Your draw commands here
    pass

Команди малювання описані в посиланні на клас CanvasItem. Їх багато.

Оновлення

Функція _draw() викликається тільки один раз, а потім команди малювання кешуються і запам'ятовуються, тому подальші виклики непотрібні.

Якщо потрібно повторне малювання, оскільки стан або щось інше змінилося, викличте CanvasItem.update() в тому ж вузлі, і відбудеться новий виклик _draw().

Ось трохи складніший приклад, змінна текстури, яка буде перемальована при зміні:

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.
    update()  # Update the node's visual representation.

func _draw():
    draw_texture(texture, Vector2())

У деяких випадках може виникнути бажання намалювати кожен кадр. Для цього просто викличте update() з _process(), ось так:

extends Node2D

func _draw():
    # Your draw commands here
    pass

func _process(delta):
    update()

Приклад: малювання кругових дуг

Тепер ми будемо використовувати спеціальну функціональність малювання Godot Engine, щоб намалювати те, для чого Godot не надає функцій. Як приклад, Godot надає функцію draw_circle(), яка малює ціле коло. Однак, як щодо малювання частини кола? Вам доведеться закодувати функцію, щоб виконати її і намалювати частину кола самостійно.

Функція дуги

Дуга визначається її параметрами опорного кола, тобто центральним положенням і радіусом. Сама дуга потім визначається кутом, з якого вона починається, і кутом, під яким вона зупиняється. Це 4 аргументи, які ми повинні надати нашій функції малювання. Ми також надамо значення кольору, щоб ми могли намалювати дугу в різних кольорах, якщо захочемо.

В основному, малювання фігури на екрані вимагає, щоб вона була розкладена на певну кількість точок, пов'язаних від однієї до іншої. Як розумієте, чим з більшої кількості точок складається ваша форма, тим гладкішою вона виглядає, але тим важча вона з точки зору обробки. Загалом, якщо ваша форма величезна (або в 3D, близько до камери), треба буде намалювати більше точок, щоб досягти гладкості. І навпаки, якщо ваша форма невелика (або в 3D, далеко від камери), ви можете зменшити кількість її точок, щоб заощадити витрати на обробку; це називається рівень деталізації (LOD). У нашому прикладі ми просто будемо використовувати фіксовану кількість точок, незалежно від радіуса.

func draw_circle_arc(center, radius, angle_from, angle_to, color):
    var nb_points = 32
    var points_arc = PoolVector2Array()

    for i in range(nb_points + 1):
        var angle_point = deg2rad(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)

Пам'ятаєте кількість точок, на які повинна бути розкладена наша форма? Ми виправили це число у змінній nb_points до значення 32. Потім ініціалізуємо порожній PoolVector2Array, який є просто масивом Vector2.

Наступний крок складається з обчислення фактичних позицій цих 32 точок, які складають дугу. Це робиться в першому циклі for: ми ітеруємо кількість точок, для яких хочемо обчислити позиції, плюс одну, щоб включити останню точку. Спочатку визначаємо кут кожної точки, між початковим і кінцевим кутами.

Причина, по якій кожен кут зменшується на 90°, полягає в тому, що ми будемо обчислювати 2D позиції з кожного кута за допомогою тригонометрії (ви знаєте, косинус і синус...). Однак, щоб бути простим, і використовувати радіани, а не градуси. Кут 0° (0 радіан) починається з 3 години, хоча ми хочемо почати підрахунок з 12 години. Таким чином, ми зменшуємо кожен кут на 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)

Результат:

../../_images/result_drawarc.png

Функція полігона дуги

Ми можемо зробити ще крок далі і не тільки написати функцію, яка малює просту частину диска, визначену дугою, але і його форму. Метод точно такий же, як і раніше, за винятком того, що ми малюємо багатокутник замість ліній:

func draw_circle_arc_poly(center, radius, angle_from, angle_to, color):
    var nb_points = 32
    var points_arc = PoolVector2Array()
    points_arc.push_back(center)
    var colors = PoolColorArray([color])

    for i in range(nb_points + 1):
        var angle_point = deg2rad(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)
../../_images/result_drawarc_poly.png

Динаміка власного малювання

Гаразд, тепер ми можемо малювати власні речі на екрані. Однак малюнок статичний; давайте зробимо так, щоб ця форма обернулася по центру. Рішення полягає в тому, щоб просто змінити з плином часу значення 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

Ми вносимо ці змінюючі значення в функції _process(delta).

Ми також збільшуємо тут наші змінні angle_from та angle_to. Однак не можна забувати про отримані значення від 0 до 360°! Тобто, якщо кут дорівнює 361°, то насправді це 1°. Якщо ви не обернете ці значення, скрипт буде працювати правильно, але кутові значення будуть рости все більше і більше з часом, поки не досягнуть максимального цілого значення, яким Godot може керувати (2^31 - 1). Коли це відбудеться, Godot може аварійно зазнати краху або викликати несподівану поведінку.

Нарешті, не можна забувати викликати функцію update(), яка автоматично викликається _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)
    update()

Крім того, не забудьте змінити функцію _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 )

Давайте запустимо! Це працює, але дуга обертається шалено швидко! Що не так?

Причина в тому, що ваш графічний процесор насправді відображає кадри так швидко, як може. Потрібно "нормалізувати" малюнок на цій швидкості; для цього ми повинні використовувати параметр delta функції _process(). delta містить час, що минув між двома останніми відтвореними кадрами. Як правило, він невеликий (близько 0,0003 секунд, але це залежить від вашого обладнання), тому використання delta для керування вимальовуванням гарантує, що ваша програма працюватиме з однаковою швидкістю на різному обладнанні.

У нашому випадку нам просто потрібно помножити нашу змінну rotation_angle на delta в функції _process(). Таким чином, наші 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)
    update()

Давайте запустимо ще раз! На цей раз відображення обертання відбувається правильно!

Згладжування

Godot пропонує параметри методу в draw_line для ввімкнення згладжування, але він не працює надійно у всіх ситуаціях (наприклад, на мобільних/веб-платформах, або коли HDR включений). Також параметр antialiased відсутній в draw_polygon.

Як варіант, встановіть та використовуйте додаток Antialiased Line2D (який також підтримує згладжене малювання Polygon2D). Зауважте, що цей додаток опирається на вузли високого рівня, а не на функції низького рівня _draw().

Інструменти

Малювання власних вузлів також може бути бажаним під час їх запуску в редакторі. Його можна використати в якості попереднього перегляду, або візуалізації певної функції, чи поведінки. Більше про це в Running code in the editor.