Движение, ориентация и движение: пример WebXR
В этой статье мы воспользуемся информацией, представленной в предыдущих статьях нашего обучающего цикла по WebXR, чтобы создать пример, в котором анимируется вращающийся куб, вокруг которого пользователь может свободно перемещаться, используя гарнитуру виртуальной реальности, клавиатуру и/или мышь. Это поможет закрепить ваши знания о том, как работают геометрия 3D-графики и VR, а также поможет убедиться, что вы понимаете, как вместе работают функции и данные, используемые во время рендеринга XR.
Рисунок: Скриншот этого примера в действии
Ядро этого примера — вращающийся, текстурированный, освещенный куб — взято из нашего цикла обучающих материалов по WebGL; а именно, из предпоследней статьи цикла, посвященной освещению в WebGL.
При чтении этой статьи и прилагаемого исходного кода полезно помнить, что дисплей для гарнитуры виртуальной реальности — это один экран, разделенный пополам. Левая половина экрана видна только левому глазу, а правая половина — только правому глазу. Для рендеринга сцены для интерактивного отображения требуется несколько рендерингов сцены — один с точки зрения каждого глаза.
При рендеринге левого глаза XRWebGLLayer имеет свою область просмотра, настроенную для ограничения рисования левой половиной поверхности рисования. В противоположность этому, при рендеринге правого глаза область просмотра настраивается для ограничения рисования правой половиной поверхности.
Этот пример демонстрирует это, показывая холст на экране, даже при представлении сцены как интерактивного дисплея с использованием устройства XR.
Зависимости
Хотя в этом примере мы не будем полагаться на какие-либо 3D-графические фреймворки, такие как three.js и тому подобные, мы используем библиотеку glMatrix для матричной математики, которую мы использовали в других примерах в прошлом. Этот пример также импортирует полифилл WebXR, поддерживаемый рабочей группой Immersive Web, которая отвечает за спецификацию API WebXR. Импортировав этот полифилл, мы позволяем примеру работать во многих браузерах, в которых пока нет реализаций WebXR, и мы сглаживаем любые временные отклонения от спецификации, возникающие в эти еще довольно экспериментальные дни спецификации WebXR.
Параметры
В этом примере есть ряд параметров, которые можно настроить, изменив значения констант перед загрузкой в браузере. Код выглядит так:
const xRotationDegreesPerSecond = 25; const yRotationDegreesPerSecond = 15; const zRotationDegreesPerSecond = 35; const enableRotation = true; const allowMouseRotation = true; const allowKeyboardMotion = true; const enableForcePolyfill = false; //const SESSION_TYPE = "immersive-vr"; const SESSION_TYPE = "inline"; const MOUSE_SPEED = 0.003;
xRotationDegreesPerSecond-
Количество градусов поворота, применяемых вокруг оси X в секунду.
yRotationDegreesPerSecond-
Количество градусов поворота вокруг оси Y каждую секунду.
zRotationDegreesPerSecond-
Количество градусов в секунду, необходимых для поворота вокруг оси Z.
enableRotation-
Логическое значение, указывающее, включено ли вращение куба вообще.
allowMouseRotation-
Если
true, вы можете использовать мышь для изменения углов обзора по высоте и азимуту. allowKeyboardMotion-
Если
true, клавиши W, A, S и D перемещают зрителя вверх, влево, вниз и вправо, а клавиши со стрелками вверх и вниз перемещают вперёд и назад. Еслиfalse, разрешаются только изменения с помощью устройств XR. enableForcePolyfill-
Если это логическое значение
true, пример будет пытаться использовать полифилл WebXR, даже если браузер поддерживает WebXR. Еслиfalse, полифилл используется только в том случае, если браузер не реализуетnavigator.xr. SESSION_TYPE-
Тип сессии XR для создания:
inlineдля инлайн-сессии, представленной в контексте документа, иimmersive-vrдля представления сцены в гарнитуре VR. MOUSE_SPEED-
Множитель, используемый для масштабирования входных данных от мыши для управления углом обзора по высоте и азимуту.
MOVE_DISTANCE-
Расстояние для перемещения в ответ на любую клавишу, используемую для перемещения зрителя по сцене.
Примечание: Этот пример всегда отображает то, что он рендерит на экране, даже если используется immersive-vr режим. Это позволяет сравнить любые различия в рендеринге между двумя режимами и позволяет увидеть вывод в интерактивном режиме, даже если у вас нет гарнитуры.
Функции настройки и вспомогательные функции
Далее мы объявляем переменные и константы, используемые во всем приложении, начиная с тех, которые используются для хранения информации, специфичной для WebGL и WebXR:
let polyfill = null; let xrSession = null; let xrInputSources = null; let xrReferenceSpace = null; let xrButton = null; let gl = null; let animationFrameRequestID = 0; let shaderProgram = null; let programInfo = null; let buffers = null; let texture = null; let mouseYaw = 0; let mousePitch = 0;
За этим следуют набор констант, в основном содержащие различные векторы и матрицы, используемые при рендеринге сцены.
const viewerStartPosition = vec3.fromValues(0, 0, -10); const viewerStartOrientation = vec3.fromValues(0, 0, 1.0); const cubeOrientation = vec3.create(); const cubeMatrix = mat4.create(); const mouseMatrix = mat4.create(); const inverseOrientation = quat.create(); const RADIANS_PER_DEGREE = Math.PI / 180.0;
Первые две — viewerStartPosition и viewerStartOrientation — указывают, где зритель будет размещен относительно центра пространства и в каком направлении он будет первоначально смотреть. cubeOrientation будет хранить текущую ориентацию куба, в то время как cubeMatrix и mouseMatrix хранят матрицы, используемые при рендеринге сцены. inverseOrientation — это кватернион, который будет использоваться для представления вращения, применяемого к эталонному пространству объекта в кадрах, которые рендерятся.
RADIANS_PER_DEGREE — это значение, на которое нужно умножить угол в градусах, чтобы преобразовать угол в радианы.
Последние четыре объявленные переменные — это хранилища ссылок на элементы <div>, в которые мы будем выводить матрицы, когда захотим показать их пользователю.
Ведение журнала ошибок
Реализована функция LogGLError(), предоставляющая удобный способ настраиваемого вывода информации о логах ошибок, возникающих при выполнении функций WebGL.
function LogGLError(where) {
let err = gl.getError();
if (err) {
console.error(`WebGL error returned by ${where}: ${err}`);
}
}
Она принимает в качестве единственного входного значения строку where, которая используется для указания части программы, которая сгенерировала ошибку, поскольку подобные ошибки могут возникать в разных ситуациях.
Вершинный и фрагментный шейдеры
Вершинный и фрагментный шейдеры идентичны тем, которые использовались в примере для нашей статьи Освещение в WebGL. Обратитесь к ней, если вы хотите ознакомиться с исходным кодом GLSL для базовых шейдеров, используемых здесь.
Достаточно сказать, что вершинный шейдер вычисляет положение каждой вершины, исходя из начальных положений каждой вершины и преобразований, которые необходимо применить для имитации текущего положения и ориентации зрителя. Фрагментный шейдер возвращает цвет каждой вершины, интерполируя, если необходимо, значения, полученные из текстуры, и применяя эффекты освещения.
Запуск и завершение работы WebXR
При первоначальной загрузке скрипта мы устанавливаем обработчик события load, чтобы мы могли выполнить инициализацию.
window.addEventListener("load", onLoad);
function onLoad() {
xrButton = document.querySelector("#enter-xr");
xrButton.addEventListener("click", onXRButtonClick);
projectionMatrixOut = document.querySelector("#projection-matrix div");
modelMatrixOut = document.querySelector("#model-view-matrix div");
cameraMatrixOut = document.querySelector("#camera-matrix div");
mouseMatrixOut = document.querySelector("#mouse-matrix div");
if (!navigator.xr || enableForcePolyfill) {
console.log("Using the polyfill");
polyfill = new WebXRPolyfill();
}
setupXRButton();
}
Обработчик события load получает ссылку на кнопку, которая переключает WebXR в xrButton, а затем добавляет обработчик для событий click. Затем получают ссылки на четыре блока <div>, в которые мы будем выводить текущее содержимое каждой из ключевых матриц для информационных целей во время работы сцены.
Затем мы проверяем, определен ли navigator.xr. Если нет — и/или константа настройки enableForcePolyfill установлена в true — мы устанавливаем полифилл WebXR, создав экземпляр класса WebXRPolyfill.
Обработка пользовательского интерфейса запуска и завершения работы
Затем мы вызываем функцию setupXRButton(), которая отвечает за настройку кнопки «Вход/Выход из WebXR» для включения или выключения ее в зависимости от наличия поддержки WebXR для типа сессии, указанного в константе SESSION_TYPE.
function setupXRButton() {
if (navigator.xr.isSessionSupported) {
navigator.xr.isSessionSupported(SESSION_TYPE).then((supported) => {
xrButton.disabled = !supported;
});
} else {
navigator.xr
.supportsSession(SESSION_TYPE)
.then(() => {
xrButton.disabled = false;
})
.catch(() => {
xrButton.disabled = true;
});
}
}
Текст метки кнопки изменяется в коде, который фактически обрабатывает запуск и остановку сессии WebXR; мы увидим это ниже.
Сессия WebXR включается и выключается обработчиком событий click на кнопке, чья метка соответственно устанавливается на «Вход в WebXR» или «Выход из WebXR». Это выполняется обработчиком событий onXRButtonClick().
async function onXRButtonClick(event) {
if (!xrSession) {
navigator.xr.requestSession(SESSION_TYPE).then(sessionStarted);
} else {
await xrSession.end();
if (xrSession) {
sessionEnded();
}
}
}
Это начинается с проверки значения xrSession для определения, существует ли у нас объект XRSession, представляющий текущую сессию WebXR. Если нет, нажатие кнопки означает запрос включения режима WebXR, поэтому вызовите requestSession() для запроса сессии WebXR нужного типа и вызовите sessionStarted() для запуска сцены в этой сессии WebXR.
Если же текущая сессия есть, мы вызываем ее метод end() для остановки сессии.
Последнее, что мы делаем в этом коде, — это проверяем, является ли xrSession по-прежнему не-NULL. Если да, вызываем sessionEnded(), обработчик события end. Этот код не должен быть необходим, но, по-видимому, существует проблема, при которой по крайней мере некоторые браузеры не правильно генерируют событие end. Выполняя обработчик события напрямую, мы вручную завершаем процесс закрытия в этой ситуации.
Запуск сессии WebXR
Функция sessionStarted() отвечает за фактическое создание и запуск сессии, настраивая обработчики событий, компилируя и устанавливая код GLSL для вершинных и фрагментных шейдеров, а также подключая слой WebGL к сессии WebXR перед запуском цикла рендеринга. Она вызывается как обработчик промиса, возвращаемого функцией requestSession().
function sessionStarted(session) {
let refSpaceType;
xrSession = session;
xrButton.innerText = "Exit WebXR";
xrSession.addEventListener("end", sessionEnded);
let canvas = document.querySelector("canvas");
gl = canvas.getContext("webgl", { xrCompatible: true });
if (allowMouseRotation) {
canvas.addEventListener("pointermove", handlePointerMove);
canvas.addEventListener("contextmenu", (event) => {
event.preventDefault();
});
}
if (allowKeyboardMotion) {
document.addEventListener("keydown", handleKeyDown);
}
shaderProgram = initShaderProgram(gl, vsSource, fsSource);
programInfo = {
program: shaderProgram,
attribLocations: {
vertexPosition: gl.getAttribLocation(shaderProgram, "aVertexPosition"),
vertexNormal: gl.getAttribLocation(shaderProgram, "aVertexNormal"),
textureCoord: gl.getAttribLocation(shaderProgram, "aTextureCoord"),
},
uniformLocations: {
projectionMatrix: gl.getUniformLocation(
shaderProgram,
"uProjectionMatrix",
),
modelViewMatrix: gl.getUniformLocation(shaderProgram, "uModelViewMatrix"),
normalMatrix: gl.getUniformLocation(shaderProgram, "uNormalMatrix"),
uSampler: gl.getUniformLocation(shaderProgram, "uSampler"),
},
};
buffers = initBuffers(gl);
texture = loadTexture(
gl,
"https://cdn.glitch.com/a9381af1-18a9-495e-ad01-afddfd15d000%2Ffirefox-logo-solid.png?v=1575659351244",
);
xrSession.updateRenderState({
baseLayer: new XRWebGLLayer(xrSession, gl),
});
const isImmersiveVr = SESSION_TYPE === "immersive-vr";
refSpaceType = isImmersiveVr ? "local" : "viewer";
mat4.fromTranslation(cubeMatrix, viewerStartPosition);
vec3.copy(cubeOrientation, viewerStartOrientation);
xrSession.requestReferenceSpace(refSpaceType).then((refSpace) => {
xrReferenceSpace = refSpace.getOffsetReferenceSpace(
new XRRigidTransform(viewerStartPosition, cubeOrientation),
);
animationFrameRequestID = xrSession.requestAnimationFrame(drawFrame);
});
return xrSession;
}
После сохранения нового объекта XRSession в xrSession, метка кнопки меняется на "Выход из WebXR", чтобы указать её новую функцию после запуска сцены, и устанавливается обработчик для события end, так что мы получаем уведомление о завершении сессии XRSession.
Затем мы получаем ссылку на <canvas> в нашем HTML-документе — а также его контекст рендеринга WebGL — который будет использоваться в качестве поверхности отрисовки сцены. Свойство xrCompatible запрашивается при вызове getContext() для элемента, чтобы получить доступ к контексту рендеринга WebGL для холста. Это гарантирует, что контекст настроен для использования в качестве источника рендеринга WebXR.
Далее, мы добавляем обработчики событий для mousemove и contextmenu, но только если константа allowMouseRotation имеет значение true. Обработчик mousemove будет обрабатывать наклоны и повороты вида на основе перемещения мыши. Поскольку функция "" работает только при нажатой правой кнопке мыши, а нажатие правой кнопкой мыши вызывает контекстное меню, мы добавляем обработчик для события contextmenu на холст, чтобы предотвратить появление контекстного меню, когда пользователь начинает перетаскивание мыши.
Далее, мы компилируем программы шейдеров; получаем ссылки на его переменные; инициализируем буферы, которые хранят массив каждой позиции; индексы в таблице позиций для каждой вершины; вершинные нормали; и координаты текстуры для каждой вершины. Все это взято непосредственно из кода примера WebGL, поэтому обратитесь к Освещение в WebGL и предыдущим статьям Создание 3D-объектов с помощью WebGL и Использование текстур в WebGL. Затем вызывается наша функция loadTexture() для загрузки файла текстуры.
Теперь, когда структуры и данные рендеринга загружены, мы начинаем подготовку к запуску XRSession. Мы подключаем сессию к слою WebGL, чтобы он знал, что использовать в качестве поверхности рендеринга, вызывая XRSession.updateRenderState() с baseLayer установленным в новый XRWebGLLayer.
Затем мы смотрим значение константы SESSION_TYPE, чтобы определить, должна ли сессия WebXR быть интерактивной или встроенной. Интерактивные сессии используют local пространство ссылок, а встроенные сессии используют viewer пространство ссылок.
Функция библиотеки glMatrix для 4x4 матриц используется для преобразования начальной позиции зрителя, заданной в константе viewerStartPosition, в матрицу преобразования, cubeMatrix. Начальная ориентация зрителя, константа viewerStartOrientation, копируется в cubeOrientation, которая будет использоваться для отслеживания вращения куба со временем.
sessionStarted() завершает работу, вызывая метод сессии requestReferenceSpace() для получения объекта пространства ссылок, описывающего пространство, в котором создается объект. Когда промис, возвращаемый методом, разрешается в объект XRReferenceSpace, мы вызываем его метод getOffsetReferenceSpace для получения объекта пространства ссылок, представляющего систему координат объекта. Начало нового пространства находится в координатах мира, заданных viewerStartPosition, а его ориентация установлена в cubeOrientation. Затем мы сообщаем сессии, что мы готовы отрисовать кадр, вызвав её метод requestAnimationFrame(). Мы сохраняем возвращённый идентификатор запроса на случай, если нам нужно будет отменить запрос позже.
Наконец, sessionStarted() возвращает XRSession, представляющий сессию WebXR пользователя.
При завершении сессии
Когда сессия WebXR заканчивается (либо потому, что она закрывается пользователем, либо потому, что вызвана функция XRSession.end()), отправляется событие end; мы настроили его так, чтобы вызывалась функция sessionEnded().
function sessionEnded() {
xrButton.innerText = "Enter WebXR";
if (animationFrameRequestID) {
xrSession.cancelAnimationFrame(animationFrameRequestID);
animationFrameRequestID = 0;
}
xrSession = null;
}
Мы также можем вызвать sessionEnded() напрямую, если хотим программно завершить сессию WebXR. В любом случае, метка кнопки обновляется, чтобы указать, что клик запустит сессию, а затем, если есть ожидающий запрос на кадр анимации, мы отменяем его, вызвав cancelAnimationFrame.
После этого значение xrSession изменяется на NULL, чтобы указать, что мы закончили с сессией.
Реализация управления
Теперь давайте рассмотрим код, который обрабатывает события клавиатуры и мыши, чтобы преобразовать их в данные, пригодные для управления аватаром в сценарии WebXR.
Перемещение с помощью клавиатуры
Для того, чтобы пользователь мог перемещаться по 3D-миру, даже если у него нет устройства WebXR с необходимыми для перемещения по пространству входами, наш обработчик событий keydown, handleKeyDown(), реагирует, обновляя смещения от начала объекта в зависимости от нажатой клавиши.
function handleKeyDown(event) {
switch (event.key) {
case "w":
case "W":
verticalDistance -= MOVE_DISTANCE;
break;
case "s":
case "S":
verticalDistance += MOVE_DISTANCE;
break;
case "a":
case "A":
transverseDistance += MOVE_DISTANCE;
break;
case "d":
case "D":
transverseDistance -= MOVE_DISTANCE;
break;
case "ArrowUp":
axialDistance += MOVE_DISTANCE;
break;
case "ArrowDown":
axialDistance -= MOVE_DISTANCE;
break;
case "r":
case "R":
transverseDistance = axialDistance = verticalDistance = 0;
mouseYaw = mousePitch = 0;
break;
default:
break;
}
}
Клавиши и их эффекты:
- Клавиша W перемещает зрителя вверх на
MOVE_DISTANCE. - Клавиша S перемещает зрителя вниз на
MOVE_DISTANCE. - Клавиша A сдвигает зрителя влево на
MOVE_DISTANCE. - Клавиша D сдвигает зрителя вправо на
MOVE_DISTANCE. - Клавиша со стрелкой вверх ↑ сдвигает зрителя вперёд на
MOVE_DISTANCE. - Клавиша со стрелкой вниз ↓ сдвигает зрителя назад на
MOVE_DISTANCE. - Клавиша R сбрасывает зрителя до начальной позиции и ориентации, сбрасывая все входные смещения до 0.
Эти смещения будут применены рендерером, начиная с следующего отрисованного кадра.
Наклон и поворот с помощью мыши
У нас также есть обработчик события mousemove, который проверяет, нажата ли правая кнопка мыши, и, если да, вызывает функцию rotateViewBy(), определённую далее, для вычисления и сохранения новых значений наклона (вверх-вниз) и поворота (влево-вправо).
function handlePointerMove(event) {
if (event.buttons & 2) {
rotateViewBy(event.movementX, event.movementY);
}
}
Вычисление новых значений наклона и поворота выполняется функцией rotateViewBy():
function rotateViewBy(dx, dy) {
mouseYaw -= dx * MOUSE_SPEED;
mousePitch -= dy * MOUSE_SPEED;
if (mousePitch < -Math.PI * 0.5) {
mousePitch = -Math.PI * 0.5;
} else if (mousePitch > Math.PI * 0.5) {
mousePitch = Math.PI * 0.5;
}
}
При вводе значений дельт мыши, dx и dy, новое значение поворота вычисляется путём вычитания из текущего значения mouseYaw произведения dx и коэффициента масштабирования MOUSE_SPEED. Таким образом, вы можете контролировать чувствительность мыши, увеличивая значение MOUSE_SPEED.
Отрисовка кадра
Наша функция обратного вызова для XRSession.requestAnimationFrame() реализована в функции drawFrame() ниже. Её задача — получить справочное пространство зрителя, рассчитать величину перемещения, необходимую для любых анимированных объектов с учётом времени, прошедшего с момента последнего кадра, а затем отобразить каждый из представлений, указанных в XRPose зрителя.
let lastFrameTime = 0;
function drawFrame(time, frame) {
const session = frame.session;
let adjustedRefSpace = xrReferenceSpace;
let pose = null;
animationFrameRequestID = session.requestAnimationFrame(drawFrame);
adjustedRefSpace = applyViewerControls(xrReferenceSpace);
pose = frame.getViewerPose(adjustedRefSpace);
if (pose) {
const glLayer = session.renderState.baseLayer;
gl.bindFramebuffer(gl.FRAMEBUFFER, glLayer.framebuffer);
LogGLError("bindFrameBuffer");
gl.clearColor(0, 0, 0, 1.0);
gl.clearDepth(1.0); // Clear everything
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
LogGLError("glClear");
const deltaTime = (time - lastFrameTime) * 0.001; // Convert to seconds
lastFrameTime = time;
for (const view of pose.views) {
const viewport = glLayer.getViewport(view);
gl.viewport(viewport.x, viewport.y, viewport.width, viewport.height);
LogGLError(`Setting viewport for eye: ${view.eye}`);
gl.canvas.width = viewport.width * pose.views.length;
gl.canvas.height = viewport.height;
renderScene(gl, view, programInfo, buffers, texture, deltaTime);
}
}
}
Первым делом мы вызываем requestAnimationFrame(), чтобы запросить повторный вызов drawFrame() для отображения следующего кадра. Затем мы передаём справочное пространство объекта в функцию applyViewerControls() , которая возвращает переработанное XRReferenceSpace, преобразующее положение и ориентацию объекта с учётом перемещений, наклона и поворота, заданных пользователем с помощью клавиатуры и мыши. Помните, что, как всегда, перемещаются и переориентируются объекты мира, а не зритель. Возвращённое справочное пространство облегчает выполнение этой задачи.
Получив новое справочное пространство, мы получаем XRViewerPose, представляющий точку зрения зрителя — для обоих его глаз. Если это удалось, мы начинаем подготовку к отображению, получив XRWebGLLayer, используемый сессией, и привязав его буфер кадров в качестве буфера кадров WebGL (чтобы отрисовка WebGL выполнялась в слое, а следовательно, и на дисплее устройства XR). Теперь, когда WebGL настроен на вывод на устройство XR, мы очищаем кадр до чёрного цвета и готовы к началу отрисовки.
Время, прошедшее с момента отображения предыдущего кадра (в секундах), вычисляется путём вычитания временной метки предыдущего кадра, lastFrameTime, из текущего времени, заданного параметром time, а затем умножения на 0,001 для перевода миллисекунд в секунды. Текущее время сохраняется в lastFrameTime;
Функция drawFrame() завершается итерацией по каждому представлению, найденному в XRViewerPose, настройкой области просмотра для представления и вызовом renderScene() для отображения кадра. Установив область просмотра для каждого представления, мы обрабатываем типичный случай, когда представления для каждого глаза отображаются на половине буфера кадров WebGL. Затем оборудование XR позаботится о том, чтобы каждый глаз видел только ту часть изображения, которая предназначена для него.
Примечание: В этом примере кадр визуализируется как на устройстве XR, так и на экране. Чтобы обеспечить правильный размер холста на экране для этого, мы устанавливаем его ширину равной ширине отдельных XRView, умноженной на количество представлений; высота холста всегда совпадает с высотой области просмотра. Две строки кода, которые изменяют размер холста, не нужны в обычных циклах рендеринга WebXR.
Применение пользовательских входных данных
Функция applyViewerControls(), вызываемая функцией drawFrame() перед началом рендеринга, принимает смещения по каждому из трёх направлений, смещение поворота по оси Y и смещение поворота по оси X, записанные функциями handleKeyDown() и handlePointerMove() в ответ на нажатие пользователем клавиш и перемещение мыши с нажатой правой кнопкой. На вход подаётся базовое справочное пространство для объекта, а на выходе возвращается новое справочное пространство, которое изменяет положение и ориентацию объекта в соответствии с результатами входных данных.
function applyViewerControls(refSpace) {
if (
!mouseYaw &&
!mousePitch &&
!axialDistance &&
!transverseDistance &&
!verticalDistance
) {
return refSpace;
}
quat.identity(inverseOrientation);
quat.rotateX(inverseOrientation, inverseOrientation, -mousePitch);
quat.rotateY(inverseOrientation, inverseOrientation, -mouseYaw);
let newTransform = new XRRigidTransform(
{ x: transverseDistance, y: verticalDistance, z: axialDistance },
{
x: inverseOrientation[0],
y: inverseOrientation[1],
z: inverseOrientation[2],
w: inverseOrientation[3],
},
);
mat4.copy(mouseMatrix, newTransform.matrix);
return refSpace.getOffsetReferenceSpace(newTransform);
}
Если все смещения ввода равны нулю, мы просто возвращаем исходное справочное пространство. В противном случае мы создаём из изменений ориентации в mousePitch и mouseYaw кватернион, задающий обратную ориентацию, чтобы применение inverseOrientation к кубу корректно отражало перемещение зрителя.
Затем следует создать новый объект XRRigidTransform, представляющий преобразование, которое будет использоваться для создания нового XRReferenceSpace для перемещённого и/или переориентированного объекта. Положение — это новый вектор, чьи x, y, и z соответствуют смещениям по каждой из этих осей. Ориентация — кватернион inverseOrientation.
Мы копируем матрицу преобразования matrix в mouseMatrix, которое мы будем использовать позже для отображения матрицы отслеживания мыши пользователю (так что этот шаг обычно можно пропустить). Наконец, мы передаём XRRigidTransform в текущее XRReferenceSpace объекта, чтобы получить справочное пространство, которое интегрирует это преобразование для представления расположения куба относительно пользователя с учётом движений пользователя. Это новое справочное пространство возвращается вызывающей стороне.
Отображение сцены
Функция renderScene() используется для фактического отображения видимых в данный момент частей мира. Она вызывается один раз для каждого глаза, с немного отличающимися позициями для каждого глаза, чтобы обеспечить необходимый 3D-эффект для устройств XR.
Большая часть этого кода представляет собой типичный код рендеринга WebGL, взятый непосредственно из функции drawScene() статьи «Освещение в WebGL», и именно там вы можете найти подробности о компонентах рендеринга WebGL в этом примере (код на GitHub). Но здесь начинается код, специфичный для этого примера, поэтому мы рассмотрим эту часть подробнее.
const normalMatrix = mat4.create();
const modelViewMatrix = mat4.create();
function renderScene(gl, view, programInfo, buffers, texture, deltaTime) {
const xRotationForTime =
xRotationDegreesPerSecond * RADIANS_PER_DEGREE * deltaTime;
const yRotationForTime =
yRotationDegreesPerSecond * RADIANS_PER_DEGREE * deltaTime;
const zRotationForTime =
zRotationDegreesPerSecond * RADIANS_PER_DEGREE * deltaTime;
gl.enable(gl.DEPTH_TEST); // Enable depth testing
gl.depthFunc(gl.LEQUAL); // Near things obscure far things
if (enableRotation) {
mat4.rotate(
cubeMatrix, // destination matrix
cubeMatrix, // matrix to rotate
zRotationForTime, // amount to rotate in radians
[0, 0, 1],
); // axis to rotate around (Z)
mat4.rotate(
cubeMatrix, // destination matrix
cubeMatrix, // matrix to rotate
yRotationForTime, // amount to rotate in radians
[0, 1, 0],
); // axis to rotate around (Y)
mat4.rotate(
cubeMatrix, // destination matrix
cubeMatrix, // matrix to rotate
xRotationForTime, // amount to rotate in radians
[1, 0, 0],
); // axis to rotate around (X)
}
mat4.multiply(modelViewMatrix, view.transform.inverse.matrix, cubeMatrix);
mat4.invert(normalMatrix, modelViewMatrix);
mat4.transpose(normalMatrix, normalMatrix);
displayMatrix(view.projectionMatrix, 4, projectionMatrixOut);
displayMatrix(modelViewMatrix, 4, modelMatrixOut);
displayMatrix(view.transform.matrix, 4, cameraMatrixOut);
displayMatrix(mouseMatrix, 4, mouseMatrixOut);
{
const numComponents = 3;
const type = gl.FLOAT;
const normalize = false;
const stride = 0;
const offset = 0;
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.position);
gl.vertexAttribPointer(
programInfo.attribLocations.vertexPosition,
numComponents,
type,
normalize,
stride,
offset,
);
gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition);
}
{
const numComponents = 2;
const type = gl.FLOAT;
const normalize = false;
const stride = 0;
const offset = 0;
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.textureCoord);
gl.vertexAttribPointer(
programInfo.attribLocations.textureCoord,
numComponents,
type,
normalize,
stride,
offset,
);
gl.enableVertexAttribArray(programInfo.attribLocations.textureCoord);
}
{
const numComponents = 3;
const type = gl.FLOAT;
const normalize = false;
const stride = 0;
const offset = 0;
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.normal);
gl.vertexAttribPointer(
programInfo.attribLocations.vertexNormal,
numComponents,
type,
normalize,
stride,
offset,
);
gl.enableVertexAttribArray(programInfo.attribLocations.vertexNormal);
}
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffers.indices);
gl.useProgram(programInfo.program);
gl.uniformMatrix4fv(
programInfo.uniformLocations.projectionMatrix,
false,
view.projectionMatrix,
);
gl.uniformMatrix4fv(
programInfo.uniformLocations.modelViewMatrix,
false,
modelViewMatrix,
);
gl.uniformMatrix4fv(
programInfo.uniformLocations.normalMatrix,
false,
normalMatrix,
);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.uniform1i(programInfo.uniformLocations.uSampler, 0);
{
const vertexCount = 36;
const type = gl.UNSIGNED_SHORT;
const offset = 0;
gl.drawElements(gl.TRIANGLES, vertexCount, type, offset);
}
}
renderScene() начинает с расчёта величины поворота вокруг каждой из трёх осей за время, прошедшее с момента отображения предыдущего кадра. Эти значения позволяют нам правильно отрегулировать поворот нашего анимированного куба, чтобы его скорость перемещения оставалась постоянной независимо от колебаний частоты кадров, которые могут возникать из-за нагрузки на систему. Эти значения вычисляются как количество радиан поворота, которое нужно применить, с учётом времени и сохраняются в константах xRotationForTime, yRotationForTime и zRotationForTime.
После включения и настройки проверки глубины мы проверяем значение константы enableRotation, чтобы увидеть, включён ли поворот куба; если это так, мы используем glMatrix для поворота cubeMatrix (представляющего текущую ориентацию куба относительно мирового пространства) вокруг трёх осей. Установив глобальную ориентацию куба, мы затем умножаем её на обратную матрицу преобразования представления, чтобы получить окончательную матрицу модели и представления — матрицу, которую нужно применить к объекту, чтобы повернуть его для анимации, а также переместить и переориентировать его для моделирования движения зрителя в пространстве.
Затем вычисляется нормальная матрица представления путём взятия матрицы модели и представления, её инвертирования и транспонирования (обмена столбцами и строками).
Последние несколько строк кода, добавленные для этого примера, представляют собой четыре вызова функции displayMatrix(), которая отображает содержимое матрицы для анализа пользователем. Остальная часть функции идентична или практически идентична более старому образцу WebGL, из которого этот код взят.
Отображение матрицы
В демонстрационных целях этот пример отображает содержимое важных матриц, используемых при рендеринге сцены. Для этого используется функция displayMatrix(); эта функция использует MathML для отображения матрицы, переходя к более массивоподобному формату, если MathML не поддерживается браузером пользователя.
function displayMatrix(mat, rowLength, target) {
let outHTML = "";
if (mat && rowLength && rowLength <= mat.length) {
let numRows = mat.length / rowLength;
outHTML = "<math display='block'>\n<mrow>\n<mo>[</mo>\n<mtable>\n";
for (let y = 0; y < numRows; y++) {
outHTML += "<mtr>\n";
for (let x = 0; x < rowLength; x++) {
outHTML += `<mtd><mn>${mat[x * rowLength + y].toFixed(2)}</mn></mtd>\n`;
}
outHTML += "</mtr>\n";
}
outHTML += "</mtable>\n<mo>]</mo>\n</mrow>\n</math>";
}
target.innerHTML = outHTML;
}
Это заменяет содержимое элемента, указанного в target, на недавно созданный элемент <math>, который содержит 4x4 матрицу. Каждая запись отображается с точностью до двух десятичных знаков.
Остальное
Остальной код идентичен тому, что представлен в предыдущих примерах:
initShaderProgram()-
Инициализирует программу GLSL шейдера, вызывая
loadShader()для загрузки и компиляции программы каждого шейдера, а затем прикрепляет каждый из них к контексту WebGL. После компиляции программа связывается и возвращается вызывающей стороне. loadShader()-
Создаёт объект шейдера и загружает указанный исходный код в него, прежде чем скомпилировать код и проверить успешность компиляции, прежде чем вернуть скомпилированный шейдер вызывающей стороне. Если произошла ошибка, возвращается
NULL. initBuffers()-
Инициализирует буферы, содержащие данные, которые будут переданы в WebGL. Эти буферы включают массив вершинных позиций, массив вершинных нормалей, координаты текстур для каждой поверхности куба и массив вершинных индексов (указывающий, какая запись в списке вершин представляет каждый угол куба).
loadTexture()-
Загружает изображение по указанному URL и создаёт текстуру WebGL из него. Если размеры изображения не являются степенями двойки (см. функцию
isPowerOf2()), сглаживание отменяется, а обёртка фиксируется к краям. Это связано с тем, что оптимизированный рендеринг сглаженных текстур в WebGL 1 работает только для текстур, размеры которых являются степенями двойки. WebGL 2 поддерживает текстуры произвольного размера для сглаживания. isPowerOf2()-
Возвращает
true, если указанное значение является степенью двойки, в противном случае возвращаетfalse.
Объединение всего
При добавлении всего этого кода, HTML и другого JavaScript-кода, не включённого выше, вы получите то, что видите, когда попробуете этот пример на Glitch. Помните: если вы заблудитесь во время перемещения, просто нажмите клавишу R, чтобы вернуться к началу.
Совет: если у вас нет устройства XR, вы можете получить некоторые 3D-эффекты, если приблизите лицо к экрану, расположив нос посередине между изображениями для левого и правого глаза на холсте. Старательно фокусируясь на изображении через экран и медленно перемещаясь вперёд и назад, вы сможете в конечном счёте сфокусироваться на 3D-изображении. Это может потребовать практики, и ваш нос может буквально коснуться экрана, в зависимости от остроты вашего зрения.
Вы можете сделать множество вещей, используя этот пример в качестве отправной точки. Попробуйте добавить больше объектов в мир или улучшить управление движением для более реалистичного перемещения. Добавьте стены, потолок и пол, чтобы ограничить пространство вместо бесконечной, как кажется, вселенной, в которой можно заблудиться. Добавьте проверку столкновений или проверки попадания, или возможность изменения текстуры каждой грани куба.
Возможностей для реализации практически нет никаких ограничений.
См. также
- Learn WebGL (включает отличные визуализации камеры и её связи с виртуальным миром)
- WebGL Fundamentals
- Learn OpenGL
© 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/Movement_and_motion