Пользовательская отрисовка в 2D

Введение

В Godot есть узлы для рисования спрайтов, многоугольников, частиц и прочего. В большинстве случаев этого достаточно; но не всегда. Прежде чем плакать от страха, тоски и ярости из-за того, что узла для рисования этого конкретного чего-то не существует... было бы хорошо знать, что можно легко заставить любой 2D-узел (на основе Control < class_Control> `или :ref: Node2D <class_Node2D> `) рисовать пользовательские команды. Также это действительно легко сделать.

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.

  • Визуализации, несовместимые с узлами, как доска тетриса. (В примере с тетрисом для рисования блоков используется пользовательская функция рисования блоков.)

  • 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.

  • Making a custom UI control. There are plenty of controls available, but when you have unusual needs, you will likely need a custom control.

Отрисовка

Добавьте сценарий к любому производному узлу CanvasItem, например Control или Node2D. Затем переопределите функцию _draw ().

extends Node2D

func _draw():
    # Your draw commands here
    pass

Команды рисования описаны в справочнике по классу CanvasItem. Их много.

Обновление

Функция _draw () вызывается только один раз, а затем команды рисования кэшируются и запоминаются, поэтому дальнейшие вызовы не нужны.

If re-drawing is required because a state or something else changed, call CanvasItem.update() in that same node and a new _draw() call will happen.

Вот немного более сложный пример, переменная текстуры, которая будет перерисована при изменении:

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 ``s.

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

Причина, по которой каждый угол уменьшается на 90°, заключается в том, что мы будем вычислять 2D-положения для каждого угла, используя тригонометрию (вы знаете, косинус и синус...). Однако для простоты cos () и sin () используют радианы, а не градусы. Угол 0° (0 радиан) начинается с 3 часов, хотя мы хотим начать отсчет с 12 часов. Таким образом, мы уменьшаем каждый угол на 90 °, чтобы начать отсчет с 12 часов.

Фактическое положение точки, расположенной на окружности под углом angle (в радианах), определяется как Vector2(cos (angle), sin (angle)). Так как cos () и sin () возвращают значения от -1 до 1, позиция находится на окружности с радиусом 1. Чтобы это положение было на нашей опорной окружности, которая имеет радиус radius, нам просто нужно умножить позицию на radius. Наконец, нам нужно расположить наш опорный круг в центральном положении, что выполняется путем добавления его к нашему значению 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. Однако мы не должны забывать применять wrap() в диапазоне от 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()

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

Antialiased drawing

Godot offers method parameters in draw_line to enable antialiasing, but it doesn't work reliably in all situations (for instance, on mobile/web platforms, or when HDR is enabled). There is also no antialiased parameter available in draw_polygon.

As a workaround, install and use the Antialiased Line2D add-on (which also supports antialiased Polygon2D drawing). Note that this add-on relies on high-level nodes, rather than low-level _draw() functions.

Инструменты

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 Запуск кода в редакторе for more information.