Каждый объект, отображаемый на экране движком: спрайты, модели, тайлы, частицы или GUI-ноды, отрисовываются рендерером (визуализатором). Сердцем любого рендерера является рендер-скрипт, который управляет конвейером рендеринга. По-умолчанию, каждый 2D объект отрисовывается с правильной битовой картой с заданным смешиванием и с правильной Z глубиной — таким образом вам возможно даже не нужно задумываться о рендеринге помимо упорядочивания и простого смешивания. Для большинства 2D игр, конвейер по-умолчанию вполне хорошо работает, но ваша игра может иметь особые требования. Если это ваш случай, Defold позволяет вам писать индивидуальные рендеринг конвейеры.
Конвейер рендеринга диктует что рендерить, когда это рендерить и также где это следует рендерить. Что рендерить задается рендер предикатами. Когда рендерить предикат управляется рендер скриптом, а где рендерить предикат управляется проекцией вида.
Рендер файл содержит ссылку на текущий рендер скрипт, а также пользовательские материалы, которые должны быть доступны в рендер скрипте (применяется вызов render.enable_material()
)
Сердцем рендеринг конвейера является рендер скрипт. Это Lua скрипт с функциями init()
, update()
и on_message()
и в первую очередь он применяется для взаимодействия с нижележащим OpenGL рендеринг API. Рендер скрипту отводится специальное место в жизненном цикле вашей игры. С деталями можно ознакомиться в Документации по жизненному циклу приложения.
В папке “Builtins” вашего проекта вы можете найти рендеринг ресурс по-умолчанию (“default.render”), а также рендер скрипт по-умолчанию (“default.render_script”).
Для настройки пользовательского рендерера:
Скопируйте файлы “default.render” и “default.render_script” в место в пределах иерархии вашего проекта. Вы, конечно же, можете создать рендер скрипт с нуля, но хорошей идеей будет начать с копии стандартного скрипта, особенно если вы новичок в Defold и/или рендеринге через OpenGL ES.
Отредактируйте вашу копию файла “default.render” и замените свойство Script, чтобы оно ссылалось на вашу копию рендер скрипта.
Замените свойство Render (в bootstrap) в файле настроек game.project так, чтобы оно ссылалось на вашу копию файла “default.render”.
Чтобы иметь возможность управлять порядком отрисовки объектов, создаются рендер предикаты. Предикат задает, что должно быть отрисовано на основе выборки тегов материала.
Каждый объект, который отрисовывается на экран, обладает прикрепленным материалом, который управляет тем, как объект должен быть отрисован на экране. В материале вы задаете один или больше тегов, которые должны быть ассоциированы с этим материалом.
В вашем рендер скрипте вы далее можете создать рендер предикат и задать, какие теги должны принадлежать этому предикату. Когда вы просите движок отрисовать предикат, каждый объект с материалом, содержащим тег, совпадающий со списком заданным для предиката, будет отрисован.
Детальное описание работы материалов может быть найдено в Документации по материалам.
Рендер-скрипт по-умолчанию сконфигурирован использовать ортографическую проекцию подходящую для 2D игр. Он предоставляет 3 разных ортографических проекции: Stretch
(применяется по-умолчанию), Fixed Fit
и Fixed
.
Также вы можете использовать проекцию перспективы, предусмотренную компонентом камеры, подходящую для 3D игр.
Компонент камеры может быть использован как для ортографической, так и перспективной проекций для изменения матрицы вида (по сути, какая часть игрового мира отображается). Узнайте больше о компоненте камеры в Руководстве по камере.
Stretch проекция всегда рисует область вашей игры равной заданным в game.project размерам, даже когда окно изменяет размер. Если соотношение сторон меняется, это отразится на содержимом игры, которое растянется или вертикально или горизонтально:
Stretch проекция с исходными размерами окна
Stretch проекция с размерами окна растянутыми по горизонтали
Stretch проекция — это проекция по-умолчанию, но если вы сменили ее на другую и вам требуется вернуться к ней обратно, вы можете сделать это послав сообщение рендер скрипту:
msg.post("@render:", "use_stretch_projection", { near = -1, far = 1 })
Как и stretch проекция, fixed fit проекция всегда отображает область игры равной заданным в game.project размерам, но если окно меняет размеры и соотношение сторон меняется, игровое содержимое сохраняет свое изначальное соотношение сторон, а также будет отображено дополнительное содержимое игры, вертикально или горизонтально:
Fixed fit проекция с исходными размерами окна
Fixed fit проекция с окном растянутым горизонтально
Fixed fit проекция с окном сжатым до 50% от исходного размера
Для включения fixed fit проекции, вы посылаете следующее сообщение рендер скрипту:
msg.post("@render:", "use_fixed_fit_projection", { near = -1, far = 1 })
Fixed проекция сохранит исходное соотношение сторон и отрендерит содержимое вашей игры с фиксированным уровнем приближения. Это означает, что если приближение установлено в значение отличное от 100%, проекция покажет либо большую либо меньшую область, чем область игры заданную размерами в game.project:
Fixed проекция с приближением в 2 раза
Fixed проекция с приближением в 0.5 раз
Fixed проекция с приближением выставленным в 2 раза и окном сжатым до 50% от исходного размера
Для включения fixed проекции, вы посылаете следующее сообщение рендер скрипту:
msg.post("@render:", "use_fixed_projection", { near = -1, far = 1, zoom = 2 })
Проекция перспективы подходит для трехмерных игр, где игровые объекты рендерятся в перспективе и где размер объектов варьируется в зависимости от расстояния от положения воображаемого глаза/камеры.
Для включения проекции перспективы, предоставляемой компонентом камеры, вы посылаете следующее сообщение рендер скрипту:
msg.post("@render:", "use_camera_projection")
Когда компоненты рендерятся, обычно обговаривается, в какой системе координат рендерятся эти компоненты. В большинстве игр у вас имеется часть компонентов рисуемых в пространстве мира игры и часть в пространстве экрана.
GUI компоненты и их ноды обычно отрисовываются в пространстве координат экрана, с координатами нижнего левого угла экрана (0,0) и координатами верхнего правого угла экрана (ширина экрана, высота экрана). Пространство системы координат экрана никогда не смещается или каким-либо другим образом не преобразовывается камерой. Таким образом GUI-ноды всегда отрисовываются на экране вне зависимости от того, как рендерится мир.
Спрайты, тайловые карты и другие компоненты, используемые игровыми объектами, существующими в мире вашей игры обычно рисуются в системе координат мирового пространства. Если вы не делаете никаких модификаций над своим рендер скриптом и не используете никакой компонент камеры для изменения проекции вида, эта система координат будет такой же как система координат экранного пространства, но как только вы добавляете камеру и либо двигаете ее или меняете проекцию вида, две системы координат отклоняются друг от друга. Когда камера движется, нижний левый угол экрана будет смещен из точки (0,0), так что рендерятся другие части мира. Если меняется проекция, координаты будут одновременно преобразованы (то есть смещены из 0, 0) и изменены масштабирующим коэффициентом.
Ниже представлен код для пользовательского рендер скрипта, который является слегка измененной версией встроенного.
init()
применяется для установления предикатов, вида и цвета очистки. Эти переменные будут использоваться во время непосредственно рендеринга.
function init(self)
-- Задаем рендер предикаты. Каждый предикат отрисовывается сам по себе и
-- это позволяет нам менять состояние OpenGL между отрисовками.
self.predicates = create_predicates("tile", "gui", "text", "particle", "model")
-- Создание и заполнение таблиц которые будут использованы в update()
local state = create_state()
self.state = state
local camera_world = create_camera(state, "camera_world", true)
init_camera(camera_world, get_stretch_projection)
local camera_gui = create_camera(state, "camera_gui")
init_camera(camera_gui, get_gui_projection)
update_state(state)
end
update()
вызывается один раз каждый кадр. Ее назначение — это производить реальную отрисовку вызывая нижележащие API OpenGL ES (OpenGL Embedded Systems API). Чтобы как следует понять, что происходит в функции update()
, вам нужно понять как работает OpenGL. Есть множество качественных источников по OpenGL ES. Официальный сайт является неплохой стартовой точкой. Он располагается по адресу https://www.khronos.org/opengles/
Данный пример содержит настройки необходимые для отрисовки 3D моделей. В функции init()
задан предикат self.predicates.model
. В другом месте был создан материал с тегом “model”. В примере присутствуют также компоненты моделей, которые используют этот материал:
function update(self)
local state = self.state
if not state.valid then
if not update_state(state) then
return
end
end
local predicates = self.predicates
-- Очищаем цветовой буффер цветом очистки и устанавливаем буфер глубины.
--
render.set_depth_mask(true)
render.set_stencil_mask(0xff)
render.clear(state.clear_buffers)
local camera_world = state.cameras.camera_world
render.set_viewport(0, 0, state.window_width, state.window_height)
render.set_view(camera_world.view)
render.set_projection(camera_world.proj)
-- Рендерим 3D модели
--
render.set_blend_func(render.BLEND_SRC_ALPHA, render.BLEND_ONE_MINUS_SRC_ALPHA)
render.enable_state(render.STATE_CULL_FACE)
render.enable_state(render.STATE_DEPTH_TEST)
render.set_depth_mask(true)
render.draw(predicates.model_pred)
render.set_depth_mask(false)
render.disable_state(render.STATE_DEPTH_TEST)
render.disable_state(render.STATE_CULL_FACE)
-- Рендерим 2D пространство(sprites, tilemaps, particles etc)
--
render.set_blend_func(render.BLEND_SRC_ALPHA, render.BLEND_ONE_MINUS_SRC_ALPHA)
render.enable_state(render.STATE_DEPTH_TEST)
render.enable_state(render.STATE_STENCIL_TEST)
render.enable_state(render.STATE_BLEND)
render.draw(predicates.tile)
render.draw(predicates.particle)
render.disable_state(render.STATE_STENCIL_TEST)
render.disable_state(render.STATE_DEPTH_TEST)
-- debug
render.draw_debug3d()
-- -- Рендерим GUI
--
local camera_gui = state.cameras.camera_gui
render.set_view(camera_gui.view)
render.set_projection(camera_gui.proj)
render.enable_state(render.STATE_STENCIL_TEST)
render.draw(predicates.gui, camera_gui.frustum)
render.draw(predicates.text, camera_gui.frustum)
render.disable_state(render.STATE_STENCIL_TEST)
end
Пока что данный скрипт простой и понятный. Каждый кадр он отрисовывает в одинаковой манере. Однако, иногда хочется иметь возможность внедрить состояние в рендер скрипт и производить разные операции в зависимости от состояния. Так же иногда хочется общаться с рендер скриптом из других частей кода игры.
on_message()
и получать сообщения из других частей вашей игры. Типовой сценарий, где внешний компонент посылает информацию рендер скрипту — это камера. Компонент камеры, который захватил фокус, будет автоматически в каждом кадре посылать свой вид и проекцию рендер скрипту. Это сообщение называется "set_view_projection"
:
local MSG_CLEAR_COLOR = hash("clear_color")
local MSG_WINDOW_RESIZED = hash("window_resized")
local MSG_SET_VIEW_PROJ = hash("set_view_projection")
function on_message(self, message_id, message)
if message_id == MSG_CLEAR_COLOR then
-- Ктото послал нам новый цвет очистки для использования.
self.clear_color = message.color
elseif message_id == MSG_SET_VIEW_PROJ then
-- Компонент камеры, завладевший фокусом будет посылать сообщения set_view_projection
-- в сокет @render. Мы можем использовать информацию о камере
-- для выставления отображения (и возможно проекции) для рендеринга.
-- Сейчас мы рендерим ортографически, поэтому здесь нет нужды в проекции камеры.
camera.view = message.view
self.camera_projection = message.projection or vmath.matrix4()
update_camera(camera, state)
end
end
Однако, любой скрипт или GUI скрипт может посылать сообщения рендер скрипту через специальный сокет @render
:
-- Поменять цвет очистки.
msg.post("@render:", "clear_color", { color = vmath.vector4(0.3, 0.4, 0.5, 0) })
"set_view_projection"
"window_resized"
local MSG_WINDOW_RESIZED = hash("window_resized")
function on_message(self, message_id, message)
if message_id == MSG_WINDOW_RESIZED then
-- Были изменены размеры окна. message.width и message.height содержат новые размеры.
...
end
end
"draw_line"
render.draw_debug3d()
.
-- нарисовать линию
local p1 = vmath.vector3(0, 0, 0)
local p2 = vmath.vector3(1000, 1000, 0)
local col = vmath.vector4(1, 1, 1, 1)
msg.post("@render:", "draw_line", { start_point = p1, end_point = p2, color = col } )
"draw_text"
always_on_top.font
. Системный шрифт имеет материал с тегом debug_text
и рендерится с другим текстом в рендер скрипте по-умолчанию.
-- отрисовать текстовое сообщение
local pos = vmath.vector3(500, 500, 0)
msg.post("@render:", "draw_text", { text = "Hello world!", position = pos })
Визуальный профайлер, доступный через посылку сообщения "toggle_profile"
в сокет @system
, не является частью скриптуемого рендерера. Он отрисовывается отдельно от вашего рендер скрипта.
Вызовы отрисовки (draw call) это термин, применяемый для описания процесса настройки GPU для отрисовки объекта на экран, используя текстуру и материал с опциональными дополнительными настройками. Обычно этот процесс ресурсоемок и рекомендуется обходиться как можно меньшим количеством вызовов отрисовки. Вы можете измерить число вызовов отрисовки и время, которое уходит на их рендеринг используя встроенный профилировщик.
Defold попытается собрать операцию рендеринга единым пакетом, чтобы снизить число вызовов отрисовки согласно набору правил определенных ниже. Правила отличаются для GUI компонентов и всех остальных компонентов другого типа.
Рендеринг делается на основе z-упорядочивания, от низшего к высшему. Движок начнет с сортировки списка того, что нужно отрисовать и пройдется от низшего к высшему z-значению. Каждый объект в списке будет сгруппирован в общий вызов отрисовки с предыдущим объектом, если будут выполнены следующие условия:
Это означает, что если два спрайтовых компонента в одной и той же прокси коллекции имеют смежные или такие же z-значения (и потому идут один за другим в сортированном списке), используют общую текстуру, материал и константы, они будут сгруппированы в один общий вызов отрисовки.
Рендеринг нод в GUI компоненте производится от верха к низу списка нод. Каждая нода в списке будет сгруппирована в один общий вызов отрисовки с предыдущей нодой, если будут выполнены следующие условия:
Рендеринг нод производится покомпонентно. Это означает, что ноды из разных GUI компонентов не будут объединены в пакет отрисовки.
Возможность размещать ноды в иерархии упрощает группировку нод в управляемые единицы. Но иерархии могут весьма эффектно разбить пакетный рендеринг, если вы смешиваете разные типы нод. Есть возможность более эффективно пакетировать GUI-ноды, в то же время поддерживая иерархии, используя GUI слои. Вы можете почитать о GUI слоях и как они влияют на вызовы отрисовки в руководстве по GUI.
Did you spot an error or do you have a suggestion? Please let us know on GitHub!
GITHUB