Рендеринг и обратный вызов анимации кадра WebXR
После настройки вашей WebXR среды и создания XRSession для представления текущей сессии XR среды, вам необходимо предоставить кадры сцены устройству XR для рендеринга. Эта статья описывает процесс управления кадрами XR сцены для устройства в цикле рендеринга, используя XRSession для получения объекта XRFrame, представляющего каждый кадр, который затем используется для подготовки буфера кадра для передачи устройству XR.
Прежде чем вы сможете отобразить виртуальную среду, вам необходимо установить сеанс WebXR, создав XRSession с помощью метода navigator.xr.requestSession(); вам также необходимо связать сеанс с буфером кадра и выполнить другие задачи настройки. Эти задачи настройки описаны в статье Запуск и завершение сессии WebXR.
Подготовка рендерера
После настройки сессии XR, с подключенным буфером кадра WebGL и подготовленным WebGL с данными, необходимыми для рендеринга сцены, вы можете настроить рендерер для запуска. Это начинается с получения пространства ссылок, в котором вы хотите рисовать, с началом и ориентацией, установленными в начальной позиции и направлении обзора зрителя. Получив это, вы запрашиваете, чтобы браузер вызвал вашу функцию рендеринга в следующий раз, когда ему понадобится буфер кадра для рендеринга вашей сцены. Это делается путем вызова метода XRSession requestAnimationFrame().
Запуск рендерера выглядит следующим образом:
let worldRefSpace;
async function runXR(xrSession) {
worldRefSpace = await xrSession.requestReferenceSpace("local");
if (worldRefSpace) {
viewerRefSpace = worldRefSpace.getOffsetReferenceSpace(
new XRRigidTransform(viewerStartPosition, viewerStartOrientation),
);
animationFrameRequestID = xrSession.requestAnimationFrame(myDrawFrame);
}
}
После получения пространства ссылок для погруженного мира, это создает смещенное пространство ссылок, представляющее положение и ориентацию зрителя, создавая XRRigidTransform, представляющий это положение и ориентацию, а затем вызывая метод XRReferenceSpace getOffsetReferenceSpace().
Затем первый кадр анимации планируется вызовом метода XRSession requestAnimationFrame(), предоставляя функцию обратного вызова, myDrawFrame(), чья задача состоит в рендеринге кадра.
Обратите внимание, что этот код не содержит цикла! Вместо этого код рендеринга кадров — в данном случае функция с именем myDrawFrame() — отвечает за планирование времени для отрисовки другого кадра, снова вызвав requestAnimationFrame().
Частота обновления и частота кадров
Предполагая, что вы вызвали метод XRSession requestAnimationFrame() с момента последнего обновления экрана, браузер будет вызывать ваш обратный вызов рендеринга каждого кадра всякий раз, когда он готов перерисовать окно вашего приложения или сайта. В этом контексте «перерисовка» означает процесс обеспечения того, что отображаемое содержимое экрана соответствует тому, что пытаются представить DOM и элементы в данный момент.
Частота вертикальной развертки оборудования
Когда браузер готов обновить <canvas>, в котором отображается ваше содержимое WebXR, он вызывает ваш обратный вызов рендеринга кадра, который использует указанную метку времени и любые другие релевантные данные, такие как модели и текстуры, а также состояние приложения, для рендеринга сцены — так, как она должна отображаться в указанное время — в буфер заднего плана WebGL. Когда ваш обратный вызов возвращается, браузер передает этот буфер заднего плана на дисплей или устройство XR вместе со всем остальным, что изменилось с момента последнего обновления экрана.
Исторически дисплеи обновлялись 60 раз в секунду. Это связано с ранними дисплеями, использующими форму волны тока сети переменного тока, которая циклически проходит 60 раз в секунду в США (50 в Европе) в целях синхронизации. Эта цифра имеет несколько разных названий, но они все эквивалентны или почти эквивалентны:
- Частота обновления
- Частота вертикальной развертки
- Частота вертикального погасания (VBL)
- Частота вертикальной синхронизации
Также используются и другие похожие термины, но независимо от того, как это называется, единицей измерения является Герц (Гц). Дисплей, который обновляется 60 раз в секунду, имеет частоту обновления 60 Гц. Это означает, что максимальное количество кадров, которые он может отобразить за секунду, равно 60. Независимо от того, сколько кадров в секунду вы отображаете сверх этого, только 60 из них попадут на экран за секунду.
Но не все дисплеи работают со скоростью 60 Гц; в наши дни дисплеи с более высокой производительностью начинают использовать гораздо более высокие частоты обновления. Например, дисплеи с частотой 120 Гц (или 120 кадров в секунду) становятся все более распространенными. Браузер всегда пытается обновляться с той же скоростью, что и дисплей, что означает, что на некоторых компьютерах ваш обратный вызов будет выполняться максимум 60 раз в секунду, а на других — 90 или 120 раз в секунду или даже больше, в зависимости от частоты кадров.
Время, доступное для рендеринга каждого кадра
Это делает использование максимального доступного времени между кадрами критичным. Если устройство пользователя использует дисплей с частотой 60 Гц, ваш обратный вызов будет вызываться до 60 раз в секунду, и ваша цель — гарантировать, что он не вызывается реже, чем это. Это достигается выполнением максимального количества операций вне основного потока и обеспечением максимальной эффективности обратного вызова рендеринга кадра. Разделение времени на блоки с частотой 60 Гц, при котором каждый блок используется хотя бы частично для рендеринга сцены, показано на диаграмме ниже.
Это важно, потому что по мере того, как компьютер становится все более загруженным, он может не иметь возможности точно вызывать ваш обратный вызов каждый кадр и может пропускать кадры. Это называется пропуском кадров. Это происходит, когда время, необходимое для рендеринга кадра, превышает время, доступное между кадрами, независимо от того, произошло ли задержка в рендеринге или сам рендеринг занял больше времени, чем было доступно.
На диаграмме выше кадр 3 пропущен, потому что кадр 2 не завершил рендеринг до того момента, когда кадр 3 должен был быть нарисован. Следующий отрисованный кадр будет кадр 4. Это еще одна причина, по которой полезно использовать метку времени, переданную в ваш обратный вызов рендеринга. Настраивая сцену на основе времени, а не номера кадра, вы можете гарантировать, что ваши рендерные кадры соответствуют ожидаемому, а не отстают.
Когда кадр пропускается, содержимое соответствующей области отображения не изменяется для этого прохода по циклу кадров. По этой причине случайный пропуск кадра обычно не очень заметен, но если это начинает происходить часто — особенно если несколько кадров пропускаются очень короткий промежуток времени — это может стать резким или даже сделать ваш дисплей непригодным для использования.
К счастью, вы можете легко вычислить, сколько времени вам разрешено использовать между кадрами, как 1/refreshRate секунды. То есть, разделив 1 на частоту обновления дисплея. Результирующее значение — это количество времени, доступное для рендеринга каждого кадра, чтобы не пропускать его. Например, у дисплея с частотой 60 Гц есть 1/60 секунды для рендеринга одного кадра или 0,0166667 секунды. А если частота обновления устройства составляет 120 Гц, у вас есть всего 0,00883333 секунды для рендеринга каждого кадра, если вы хотите избежать пропуска кадров.
Даже если аппаратное обеспечение фактически работает на частоте 120 Гц, вы можете обойтись, обновляясь только 60 раз в секунду, и нацеливание на это обычно является хорошей отправной точкой. 60 FPS уже выходит за пределы точки, когда большинство людей легко могут обнаружить, что анимация — это не серия неподвижных изображений, проходящих очень быстро. Другими словами, когда сомневаетесь, можете предположить, что дисплей обновляется с частотой 60 Гц. Пока ваш код написан правильно, все будет в порядке.
Озабоченность производительностью рендерера
Очевидно, у вас очень мало времени для рендеринга вашей сцены каждый кадр. Кроме того, если ваш рендерер работает дольше, чем это время, вы можете не только заставить кадр пропустить, но и потратить это время напрасно, блокируя выполнение других кодов для этого кадра.
Кроме того, если ваш рендеринг пересекает границу вертикальной развертки, вы можете получить эффект разрыва. Разрыв возникает, когда аппаратное обеспечение дисплея начинает следующий цикл обновления, в то время как предыдущий кадр все еще отображается на экране. В результате вы получаете визуальный эффект, когда верхняя часть экрана показывает новый кадр, а нижняя часть кадра показывает некоторое сочетание предыдущего кадра и, возможно, даже кадра, который был перед ним.
Ваша задача — сохранить ваш код достаточно компактным и легким, чтобы вы не превышали доступное вам время или не вызывали пропуск кадров или чрезмерное использование основного потока.
По этим причинам, если ваш рендерер не очень мал и легкий, с небольшим объемом работы, вы должны рассмотреть возможность перегрузки всего, что вы можете, на рабочий процесс, чтобы вы могли обрабатывать следующий кадр, пока браузер обрабатывает другие вещи. Имея ваши вычисления и данные готовыми до фактического вызова кадра, вы можете сделать свой сайт или приложение гораздо эффективнее, улучшить производительность основного потока и в целом улучшить пользовательский опыт.
К счастью, есть несколько хитростей, которые вы можете использовать для дальнейшего уменьшения влияния и оптимизации производительности, если ваши потребности в рендеринге особенно велики. См. руководство по производительности WebXR для рекомендаций и советов, которые помогут вам обеспечить максимальную производительность.
Кадры WebXR
Ваша функция обратного вызова рендеринга кадра получает в качестве входных данных две переменные: время, которому соответствует кадр, и объект XRFrame, описывающий состояние сцены на момент этого времени.
Оптика 3D
У нас две глаза по какой-то причине: имея два глаза, каждый из них видит мир под немного другим углом. Поскольку расстояние между ними известно и постоянно, наш мозг может выполнять простейшие геометрические и тригонометрические вычисления и определять трёхмерную природу реальности на основе этой информации. Мы также используем перспективу, различия в размерах и даже понимание того, как обычно выглядят вещи, чтобы разобраться в деталях этого третьего измерения. Эти и другие факторы являются источником нашего восприятия глубины.
Чтобы создать иллюзию трёх измерений при отрисовке графики, нам нужно смоделировать как можно больше из этих факторов. Чем больше из них мы моделируем — и чем точнее мы это делаем — тем лучше мы можем обмануть человеческий мозг, заставив его воспринимать наши изображения в 3D. Преимущество XR заключается в том, что мы можем не только использовать классические монокулярные методы для моделирования 3D-графики (перспектива, размер и моделируемая параллакс), но и смоделировать бинокулярное зрение — то есть зрение с использованием двух глаз — путем двойного отрисовки сцены для каждого кадра анимации — по одному для каждого глаза.
Типичное расстояние между зрачками человека — расстояние между центрами зрачков — составляет от 54 до 74 миллиметров (от 0,054 до 0,074 метра). Итак, если центр головы зрителя расположен в [0.0, 2.0, 0.0] (примерно в двух метрах над уровнем земли в центре пространства по горизонтали), нам сначала нужно отрисовать сцену, скажем, [-0.032, 2.0, 0.0] (32 мм слева от центра), а затем снова отрисовать её в [0.032, 2.0, 0.0] (32 мм справа от центра). Таким образом, мы размещаем положение глаз зрителя на среднем расстоянии между зрачками человека в 64 мм.
Это расстояние (или любое другое расстояние между зрачками, которое настроено в системе XR) достаточно, чтобы наш мозг увидел достаточно различий из-за ретинальной диспаратности (различия в том, что видит каждая сетчатка) и эффекта параллакса, чтобы наш мозг мог вычислить расстояние до объектов и их глубину, тем самым позволяя нам воспринимать три измерения, несмотря на то, что наши сетчатки являются только 2D-поверхностями.
Это показано на диаграмме ниже, на которой мы видим, как каждый глаз воспринимает кубик, расположенный прямо перед зрителем. Хотя эта диаграмма в некоторых аспектах преувеличивает эффект в целях наглядности, концепция остается той же. Каждый глаз видит область, границы которой образуют дугу перед глазом. Поскольку каждый глаз смещён в одну или другую сторону от центральной линии головы, и каждый глаз видит примерно одинаковое поле зрения, в результате каждый глаз видит немного другую часть мира перед собой и под немного другим углом.
Левый глаз видит кубик немного слева от центра, а правый глаз — немного справа от центра. В результате левый глаз видит немного больше левой стороны объекта и немного меньше правой, и наоборот. Эти два изображения фокусируются на сетчатках, и результирующий сигнал передаётся по зрительным нервам в зрительную кору мозга, расположенную в задней части затылочной доли.
Мозг принимает эти сигналы от левого и правого глаза и строит единое, объединённое, 3D-изображение мира в мозге зрителя, и именно это изображение и видно. И из-за этих различий между тем, что видит левый глаз, и тем, что видит правый глаз, мозг может сделать вывод о большом количестве информации о глубине объекта, его размере и многом другом. Объединив эту информацию о глубине с другими подсказками, такими как перспектива, тени, воспоминания о том, что эти отношения означают, и так далее, мы можем получить много информации о мире вокруг нас.
Кадры, позы, виды и буферы кадров
После того, как у вас есть XRFrame , представляющее состояние сцены в определённый момент времени, вам нужно определить положение объектов в сцене относительно зрителя, чтобы вы могли их отрисовать. Положение и ориентация зрителя относительно пространства отсчёта представляются с помощью XRViewerPose, полученного путём вызова метода XRFrame getViewerPose().
XRFrame не отслеживает напрямую положения или ориентации объектов в вашем мире. Вместо этого он предоставляет способ преобразования положений и ориентаций в систему координат сцены, собирает данные о положении и ориентации зрителя с оборудования XR, преобразует их в пространство отсчёта, которое вы настроите, и предоставляет их коду отрисовки кадра вместе с отметкой времени. Вы используете эту отметку времени и свои собственные данные, чтобы определить, как отрисовать сцену.
После двойной отрисовки сцены — один раз в левую половину буфера кадров и один раз в правую половину буфера кадров — буфер кадров отправляется оборудованию XR, которое отображает каждую половину буфера кадров соответствующему глазу. Это часто (но не всегда) делается путём вывода изображения на один экран и использования линз для передачи правильной половины этого изображения каждому глазу.
Дополнительную информацию о том, как 3D представляется в WebXR, можно найти в Представление 3D с помощью WebXR.
Отрисовка сцены
Когда пришло время подготовить буфер кадров для того, чтобы браузер мог нарисовать следующий кадр вашей сцены, вызывается функция, которую вы предоставили requestAnimationFrame(). В качестве входных данных она получает время, в которое рисуется кадр, и объект XRFrame, предоставляющий подробности о состоянии сцены для кадра, который вам нужно отрисовать.
В идеале, этот код должен быть достаточно быстрым, чтобы поддерживать частоту кадров 60 FPS или как можно ближе к ней, помня о том, что в этой одной функции происходит больше, чем просто ваш код. Вам нужно убедиться, что основной поток не тратит больше времени на выполнение за кадр, чем длительность самого кадра.
Базовый рендерер
В этой версии обратного вызова рендеринга WebXR мы используем очень простой подход, который отлично работает для относительно простых проектов. Этот псевдокод описывает этот процесс:
for each view in the pose's views list:
get the WebXR GL layer's viewport
set the WebGL viewport to match
for each object in the scene
bindProgram()
bindVertices()
bindMatrices()
bindUniforms()
bindBuffers()
bindTextures()
drawMyObject()
Этот тип рендерера использует порядок «сначала-вью». Каждый из двух вью, составляющих отображение устройства XR, рендерится друг за другом, где каждый объект рисуется в одном вью, прежде чем рендерить тот же набор объектов в другом вью. В результате затраты ресурсов значительны, так как большая часть данных, необходимых для отрисовки объекта, передаётся в GPU дважды за кадр. Однако это упрощает портирование существующего кода WebGL и часто достаточно для выполнения задачи, поэтому мы рассмотрим этот метод в первую очередь.
См. Оптимизацию за счёт рендеринга в порядке «сначала-объект» для альтернативного подхода, при котором каждый объект рендерится дважды друг за другом, один раз для каждого глаза, прежде чем переходить к следующему объекту сцены в кадре; то есть, рендеринг в порядке «сначала-объект».
Пример обратного вызова рендеринга
Давайте взглянем на реальный код, который следует этому основному шаблону. Поскольку в примере выше мы назвали эту функцию myDrawFrame(), мы будем продолжать использовать её здесь.
let lastFrameTime = 0;
function myDrawFrame(currentFrameTime, frame) {
const session = frame.session;
let viewerPose;
// Schedule the next frame to be painted when the time comes.
animationFrameRequestID = session.requestAnimationFrame(myDrawFrame);
// Get an XRViewerPose representing the position and
// orientation of the viewer. If successful, render the
// frame.
viewerPose = frame.getViewerPose(viewerRefSpace);
if (viewerPose) {
const glLayer = session.renderState.baseLayer;
gl.bindFrameBuffer(gl.FRAMEBUFFER, glLayer.framebuffer);
// Start by erasing the color and depth framebuffers.
gl.clearColor(0, 0, 0, 1.0);
gl.clearDepth(1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// Compute the time elapsed since the last frame was rendered.
// Use this value to ensure your animation runs at the exact
// rate you intend.
const deltaTime = currentFrameTime - lastFrameTime;
lastFrameTime = currentFrameTime;
// Now call the scene rendering code once for each of
// the session's views.
for (const view of viewerPose.views) {
const viewport = glLayer.getViewport(view);
gl.viewport(viewport.x, viewport.y, viewport.width, viewport.height);
myDrawSceneIntoView(view, deltaTime);
}
}
}
Функция myDrawFrame() получает XRSession из объекта XRFrame, указанного параметром frame, затем вызывает метод requestAnimationFrame() сессии, чтобы немедленно запланировать рендеринг следующего кадра. Это гарантирует, что мы сразу попадём в очередь, позволяя остальному времени, потраченному в этом цикле функции myDrawFrame(), учитываться в расчёте времени отрисовки следующего кадра.
Затем мы получаем объект XRViewerPose, который описывает позу зрителя — его положение и ориентацию — с помощью метода getViewerPose() кадра, передавая в него пространство отсчёта зрителя из ранее полученного при настройке сессии WebXR viewerRefSpace.
Получив позу зрителя, мы можем начать рендеринг кадра. Первый шаг — получить доступ к буферу кадра, в который устройство WebXR хочет нарисовать кадр; это делается путём получения целевого WebGL-слоя из свойства renderState объекта сессии baseLayer, а затем получения framebuffer из этого объекта XRWebGLLayer. Затем мы вызываем gl.bindFrameBuffer(), чтобы привязать этот буфер кадра в качестве цели для всех последующих команд рисования.
Следующий шаг — очистка буфера кадра. Хотя теоретически вы можете пропустить этот шаг — *если и только если ваш код рендеринга гарантирует, что он заполнит каждый пиксель буфера кадра* — обычно безопаснее очистить его перед началом рисования, если вы не стремитесь выжать максимальную производительность и знаете, что обрабатываете все пиксели. Цвет фона устанавливается в полностью непрозрачный чёрный с помощью gl.clearColor(); глубина очистки устанавливается в 1,0, вызывая gl.clearDepth(), для очистки всех пикселей независимо от того, насколько далеко расположен объект, и, наконец, пиксельные и глубинные буферы кадра очищаются с помощью вызова gl.clear(), передавая маску битов, в которой установлены и COLOR_BUFFER_BIT, и DEPTH_BUFFER_BIT.
Поскольку WebXR использует один буфер кадра для каждого вью, а области обрезки вью используются для разделения точки зрения каждого глаза внутри буфера кадра, нам нужно очистить только один буфер кадра, а не очищать его для каждого глаза (или других точек зрения, если таковые имеются).
Далее, время, прошедшее с момента рендеринга предыдущего кадра, рассчитывается путём вычитания из текущего времени, указанного параметром currentFrameTime, сохранённого времени рендеринга последнего кадра, lastFrameTime. Результат — значение DOMHighResTimeStamp, указывающее количество миллисекунд, прошедших с момента рендеринга последнего кадра. Мы можем использовать это значение при отрисовке сцены, чтобы гарантировать, что мы перемещаем всё на соответствующее расстояние, учитывая фактическое прошедшее время, а не предполагая, что вызов будет вызываться с постоянной частотой кадров. Это прошедшее время сохраняется в переменной deltaTime, а значение lastFrameTime заменяется временем этого кадра, готовым для вычисления разности для следующего кадра.
Теперь пришло время фактически рендерить сцену для каждого глаза. Мы итерируемся по массиву views позиций зрителя. Для каждого из этих объектов XRView, представляющих перспективу глаза в сцене, нам необходимо ограничить рисование областью буфера кадра, которая представляет видимое изображение текущего глаза.
Мы начинаем подготовку WebGL к рендерингу содержимого глаза, получая область обрезки, которая ограничивает рисование областью внутри буфера кадра, предназначенной для изображения текущего глаза, вызывая метод XRWebGLLayer getViewport(). Затем мы устанавливаем область обрезки WebGL соответственно, передавая координаты X и Y начала области обрезки, а также её ширину и высоту в gl.viewport().
Наконец, мы вызываем наш метод myDrawSceneIntoView() для фактического использования WebGL для рендеринга сцены. В него мы передаём XRView, представляющий глаз, для которого мы рисуем (чтобы выполнить перспективную проекцию и т. п.), и deltaTime, чтобы код рисования сцены мог точно представить прошедшее время при определении позиций движущихся объектов со временем.
Когда цикл, который итерируется по вью, заканчивается, каждое изображение, необходимое для представления сцены зрителю, было рендерировано, и при возвращении буфер кадра проходит через GPU и в конечном итоге доходит до дисплея или дисплеев устройства XR. Поскольку мы вызвали requestAnimationFrame() в начале функции, наш обратный вызов будет вызван снова, когда придёт время рендерить следующий кадр анимации сцены.
Недостатки этого подхода
Поскольку важно минимизировать время, затрачиваемое на эту функцию, чем больше времени вы тратите на обработку изменений состояния, тем меньше времени у вас остаётся на фактическое рисование. Этот метод отлично работает для небольшого количества объектов, но, поскольку он должен повторно привязывать все данные для каждого объекта дважды (один раз для левого глаза и один раз для правого), вы тратите много времени на настройку состояния, загрузку буферов и текстур и так далее. В следующем разделе мы рассмотрим изменённый подход, который значительно сокращает эти изменения состояния, обеспечивая потенциально гораздо более быстрый подход к рендерингу, особенно по мере увеличения количества объектов.
Оптимизация за счёт рендеринга в порядке «сначала-объект»
Преимущество подхода WebXR, в котором используется один WebGL-буфер кадра для хранения представлений левого и правого глаза в одном буфере кадра, позволяет существенно улучшить производительность рендеринга, переупорядочив порядок выполнения действий. Вместо того, чтобы устанавливать область обрезки для определённой точки зрения (например, левого глаза), затем рендерить каждый видимый для левого глаза объект по одному, перенастраивая буферы для каждого объекта по мере продвижения, вы можете вместо этого рендерить каждый объект дважды подряд, один раз для каждого глаза, тем самым вам потребуется настроить буферы, униформы и т. д. только один раз для обоих глаз.
Получившийся псевдокод выглядит так:
for each object in the scene
bindProgram()
bindUniforms()
bindBuffers()
bindTextures()
for each view in the pose's views list
get the XRWebGLLayer's viewport
set the WebGL viewport to match
bindVertices()
bindMatrices()
drawMyObject()
Изменив вещи таким образом, мы связываем программы, униформы, буферы, текстуры и, возможно, другие вещи только один раз за кадр вместо двух для каждого объекта в сцене. Это уменьшает издержки на потенциально очень большой процент.
Ограничение частоты кадров
Если вам нужно намеренно ограничить частоту кадров, чтобы установить базовый показатель частоты кадров, который необходимо поддерживать, предоставляя больше времени для выполнения другого кода, вы можете сделать это, намеренно пропуская кадры по расписанию.
Например, чтобы уменьшить частоту кадров на 50%, пропустите каждый второй кадр:
let tick = 0;
function drawFrame(time, frame) {
animationFrameRequestID = frame.session.requestAnimationFrame(drawFrame);
if (!(tick % 2)) {
/* Draw the scene */
}
tick++;
}
Эта версия обратного вызова рендеринга поддерживает счётчик tick. Кадр рендерится только если tick является чётным числом. Таким образом, рендерится только каждый второй кадр.
Аналогично вы можете рендерить каждый четвёртый кадр, используя !(tick % 4), и так далее.
Согласование анимации с прошедшим временем
Обратный вызов отрисовки получает параметр time по уважительной причине. Это значение DOMHighResTimeStamp — это число с плавающей запятой, указывающее время, на которое кадр был запланирован для отрисовки. Поскольку выполнение вашего обратного вызова не будет происходить с точностью до 1/60 секунды — и, действительно, может происходить с другими скоростями, если у пользователя дисплей с другой частотой кадров — вы не можете полагаться на тот факт, что ваш код выполняется, чтобы предположить, что прошло 1/60 секунды с момента последнего кадра.
По этой причине вам необходимо использовать предоставленное значение отметки времени, чтобы гарантировать, что ваша анимация отрисовывается с точно заданной скоростью. Для этого в первую очередь необходимо вычислить время, прошедшее с момента отрисовки последнего кадра:
let lastFrameTime = 0;
function drawFrame(time, frame) {
// schedule next frame, prepare the buffer, etc.
const deltaTime = (time - lastFrameTime) * 0.001;
lastFrameTime = time;
for (const view of pose.views) {
/* render each view */
}
}
Это сохраняет глобальное значение (или свойство объекта) lastFrameTime, содержащее время отрисовки предыдущего кадра. В данном случае, поскольку значения времени хранятся в миллисекундах, мы умножаем на 0,001, чтобы преобразовать время в секунды. В некоторых случаях это экономит время позже. В других ситуациях вам нужно время в миллисекундах, поэтому вам не нужно ничего менять.
Получив время, прошедшее с момента последнего кадра, ваш код отрисовки может вычислить, насколько каждый движущийся объект сместился за это время. Например, если объект вращается, вы можете применить вращение следующим образом:
const xDeltaRotation = xRotationDegreesPerSecond * RADIANS_PER_DEGREE * deltaTime; const yDeltaRotation = yRotationDegreesPerSecond * RADIANS_PER_DEGREE * deltaTime; const zDeltaRotation = zRotationDegreesPerSecond * RADIANS_PER_DEGREE * deltaTime;
Это вычисляет величину вращения объекта вокруг каждой из трех осей с момента последнего отображения кадра. Без этого фигура будет вращаться на заданный угол в каждом кадре, независимо от времени, прошедшего между кадрами. Это может привести к заметным подергиваниям во многих случаях.
Такой же принцип применим для объектов, которые движутся, а не вращаются:
const xDistanceMoved = xSpeedPerSecond * deltaTime; const yDistanceMoved = ySpeedPerSecond * deltaTime; const ZDistanceMoved = zSpeedPerSecond * deltaTime;
xSpeedPerSecond, ySpeedPerSecond, и zSpeedPerSecond содержат составляющую скорости объекта по каждой оси. Другими словами, [xDistanceMoved, yDistanceMoved, zDistanceMoved] представляет собой вектор, описывающий скорость объекта.
Дополнительные задачи, связанные с анимацией сцены
Конечно, есть и другие действия, которые, вероятно, должны происходить в каждом проходе через рендерер. Два наиболее распространенных — обработка пользовательского ввода и обновление позиций объектов (или зрителя) на основе известных факторов, таких как состояния управления пользователем или известные анимационные траектории объектов в сцене.
Обработка пользовательского ввода
Существует три способа, с помощью которых пользователи могут вводить данные при использовании веб-приложения WebXR. Во-первых, WebXR поддерживает непосредственную обработку ввода от контроллеров, интегрированных в само оборудование XR. Эти источники ввода могут включать устройства, такие как контроллеры для рук, оптические системы отслеживания, акселерометры и компасы, а также другие подобные устройства.
Второй тип ввода — джойстик, подключенный через систему XR. Он использует интерфейсы, унаследованные от API джойстика, но взаимодействует с ними через WebXR.
Третий и последний тип ввода — традиционное устройство ввода без XR, такое как клавиатура, мышь, трекпад, сенсорный экран, джойстики и геймпады без XR.
Информация об ориентации и положении, которую можно получить непосредственно от оборудования XR, применяется автоматически. Таким образом, вам необходимо самостоятельно обрабатывать другие типы ввода:
- Цель указательного устройства и нажатия кнопок
- Ввод от джойстика
- Ввод от устройств ввода без XR
Чтобы узнать больше о том, как обрабатывать пользовательский ввод при представлении сцены с помощью WebXR, см. статью Ввод и источники ввода.
Обновление позиций объектов
Большинство (хотя и не все) сцен включают какой-либо вид анимации, в котором объекты перемещаются и реагируют друг на друга соответствующим образом.
Например, в игре виртуальной или дополненной реальности может быть несколько врагов-неигровых персонажей, управляемых компьютером и перемещающихся по сцене. Не только их положение в мире меняется со временем, но и у каждого НПС, скорее всего, есть части тела или компоненты, которые движутся относительно друг друга. Руки и ноги машут, когда существо идёт, головы наклоняются и поворачиваются, волосы колышутся и развеваются, туловища расширяются и сжимаются, когда персонаж дышит.
Кроме того, в движении могут быть объекты и структуры. В спортивной игре может быть мяч, описывающий дугу в воздухе, его движение необходимо моделировать. В гоночных играх могут быть автомобили или другие транспортные средства с подвижными частями, которые нужно анимировать, включая колёса. Если в сцене есть вода, ей необходимы волны или рябь, чтобы выглядеть реалистично. Части конструкций могут двигаться, такие как двери, стены и полы (для некоторых типов игр) и так далее.
Ещё одним распространённым источником движения является сам игрок. После интерпретации ввода от контроллеров (как связанных с XR, так и других), вы должны применить эти изменения к сцене, чтобы смоделировать движение пользователя. Подробности и исчерпывающий пример того, как это работает, см. в статье Движение, ориентация и перемещение.
Следующие шаги
После того, как вы написали свой рендерер — или, по крайней мере, получили что-то работающее, даже если это не завершённый вариант — вы можете начать работать с камерой и её движением по сцене. Это описано в нашей статье о точках обзора и зрителях в WebXR.
См. также
© 2005–2024 MDN contributors.
Licensed under the Creative Commons Attribution-ShareAlike License v2.5 or later.
https://developer.mozilla.org/en-US/docs/Web/API/WebXR_Device_API/Rendering