使用 WebVR API
注意: WebVR API 已被 WebXR API 取代。WebVR 從未被批准為標準,在少數瀏覽器中預設實現和啟用,並支援少量裝置。
WebVR API 是 Web 開發人員工具包的一個絕佳補充,它允許 WebGL 場景在虛擬現實顯示器(如 Oculus Rift 和 HTC Vive)中呈現。但是,如何開始為 Web 開發 VR 應用程式呢?本文將引導您瞭解基礎知識。
入門
首先,您需要
-
支援 VR 硬體。
- 最便宜的選擇是使用移動裝置、支援的瀏覽器和裝置支架(例如 Google Cardboard)。這不會像專用硬體那樣提供良好的體驗,但您無需購買功能強大的計算機或專用 VR 顯示器。
- 專用硬體可能成本高昂,但它確實提供了更好的體驗。目前最相容 WebVR 的硬體是 HTC VIVE 和 Oculus Rift。webvr.info 的首頁提供了一些關於可用硬體和哪些瀏覽器支援它們的有用資訊。
-
如果需要,一臺功能強大的計算機,足以處理使用專用 VR 硬體進行 VR 場景的渲染/顯示。要了解您需要什麼,請查閱您要購買的 VR 的相關指南(例如,VIVE READY 計算機)。
-
安裝了支援的瀏覽器 — 目前,在桌面或移動裝置上,最新的 Firefox Nightly 或 Chrome 是您的最佳選擇。
一旦您組裝好所有裝置,您可以透過訪問我們的簡單的 A-Frame 演示,並檢視場景是否渲染以及您是否可以透過按下右下角的按鈕進入 VR 顯示模式,來測試您的設定是否與 WebVR 配合良好。
如果您想快速建立 WebVR 相容的 3D 場景,而無需瞭解一堆新的 JavaScript 程式碼,那麼 A-Frame 絕對是最佳選擇。但是,它不會教您原始 WebVR API 的工作原理,這正是我們接下來要討論的。
介紹我們的演示
為了說明 WebVR API 的工作原理,我們將研究我們的 raw-webgl-example,它看起來有點像這樣

注意: 如果 WebVR 在您的瀏覽器中不起作用,您可能需要確保它透過您的顯示卡執行。例如,對於 NVIDIA 顯示卡,如果您已成功設定 NVIDIA 控制面板,則會有一個上下文選單選項可用 — 右鍵單擊 Firefox,然後選擇使用圖形處理器執行 > 高效能 NVIDIA 處理器。
我們的演示展示了 WebGL 演示的聖盃——一個旋轉的 3D 立方體。我們使用原始 WebGL API 程式碼實現了這一點。我們不會教授任何基本的 JavaScript 或 WebGL,只教授 WebVR 部分。
我們的演示還包括
- 一個按鈕,用於開始(和停止)在 VR 顯示器中呈現我們的場景。
- 一個按鈕,用於顯示(和隱藏)VR 姿態資料,即耳機的即時更新位置和方向。
當您檢視我們演示的主要 JavaScript 檔案的原始碼時,您可以輕鬆地透過在前面的註釋中搜索字串“WebVR”來找到特定於 WebVR 的部分。
注意: 要了解更多關於基本 JavaScript 和 WebGL 的資訊,請查閱我們的 JavaScript 學習材料和我們的 WebGL 教程。
它是如何工作的?
此時,讓我們看看程式碼中 WebVR 部分是如何工作的。
一個典型(簡單)的 WebVR 應用程式是這樣工作的
Navigator.getVRDisplays()用於獲取對 VR 顯示器的引用。VRDisplay.requestPresent()用於開始向 VR 顯示器呈現內容。- WebVR 專用的
VRDisplay.requestAnimationFrame()方法用於以顯示器正確的重新整理率執行應用程式的渲染迴圈。 - 在渲染迴圈中,您獲取顯示當前幀所需的資料 (
VRDisplay.getFrameData()),將顯示的場景繪製兩次——每次為每隻眼睛的檢視繪製一次——然後透過 (VRDisplay.submitFrame()) 將渲染的檢視提交給顯示器以顯示給使用者。
在下面的部分中,我們將詳細檢視我們的 raw-webgl-demo,並瞭解上述功能具體在哪裡使用。
從一些變數開始
您將遇到的第一個與 WebVR 相關的程式碼是以下程式碼塊
// WebVR variables
const frameData = new VRFrameData();
let vrDisplay;
const btn = document.querySelector(".stop-start");
let normalSceneFrame;
let vrSceneFrame;
const poseStatsBtn = document.querySelector(".pose-stats");
const poseStatsSection = document.querySelector("section");
poseStatsSection.style.visibility = "hidden"; // hide it initially
const posStats = document.querySelector(".pos");
const orientStats = document.querySelector(".orient");
const linVelStats = document.querySelector(".lin-vel");
const linAccStats = document.querySelector(".lin-acc");
const angVelStats = document.querySelector(".ang-vel");
const angAccStats = document.querySelector(".ang-acc");
let poseStatsDisplayed = false;
我們來簡要解釋一下這些
frameData包含一個VRFrameData物件,該物件使用VRFrameData()建構函式建立。它最初是空的,但稍後將包含在 VR 顯示器中顯示每一幀所需的資料,這些資料會在渲染迴圈執行時不斷更新。vrDisplay最初未初始化,但稍後將儲存對我們 VR 頭顯的引用(VRDisplay——API 的中央控制物件)。btn和poseStatsBtn儲存了我們用於控制應用程式的兩個按鈕的引用。normalSceneFrame和vrSceneFrame最初未初始化,但稍後將儲存對Window.requestAnimationFrame()和VRDisplay.requestAnimationFrame()呼叫的引用——這些將啟動一個正常渲染迴圈和一個特殊的 WebVR 渲染迴圈;我們稍後將解釋這兩者之間的區別。- 其他變數儲存了 VR 姿態資料顯示框不同部分的引用,您可以在 UI 的右下角看到它。
獲取對我們 VR 顯示器的引用
首先,我們獲取一個 WebGL 上下文,用於將 3D 圖形渲染到我們 HTML 中的 <canvas> 元素。然後我們檢查 gl 上下文是否可用 — 如果可用,我們執行一些函式來設定場景以供顯示。
const canvas = document.getElementById("gl-canvas");
initWebGL(canvas); // Initialize the GL context
// WebGL setup code here
接下來,我們透過將畫布設定為填充整個瀏覽器視口,並首次執行渲染迴圈(drawScene()),開始實際將場景渲染到畫布上。這是非 WebVR — 正常 — 渲染迴圈。
// draw the scene normally, without WebVR - for those who don't have it and want to see the scene in their browser
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
drawScene();
現在來看我們的第一個 WebVR 特定程式碼。首先,我們檢查 Navigator.getVRDisplays 是否存在 — 這是 API 的入口點,因此是 WebVR 的良好基本特性檢測。如果它不存在,我們會記錄一條訊息,表明瀏覽器不支援 WebVR 1.1。
// WebVR: Check to see if WebVR is supported
if (navigator.getVRDisplays) {
console.log("WebVR 1.1 supported");
// ...
} else {
console.log("WebVR API not supported by this browser.");
}
其餘程式碼都放在 if (navigator.getVRDisplays) { } 塊中,這樣它只在支援 WebVR 時執行。
我們首先執行 Navigator.getVRDisplays() 函式。它返回一個 Promise,該 Promise 在成功時返回一個數組,其中包含連線到計算機的所有 VR 顯示裝置。如果沒有連線裝置,則陣列將為空。
在 Promise 的 then() 塊中,我們檢查陣列長度是否大於 0;如果是,我們將 vrDisplay 變數的值設定為陣列中的第 0 個元素。vrDisplay 現在包含一個表示我們已連線顯示器的 VRDisplay 物件!
// Then get the displays attached to the computer
navigator.getVRDisplays().then((displays) => {
// If a display is available, use it to present the scene
if (displays.length > 0) {
vrDisplay = displays[0];
console.log("Display found");
// ...
}
});
其餘程式碼都放在 if (displays.length > 0) { } 塊中,因此它只在至少有一個 VR 顯示器可用時執行。
注意: 您不太可能將多個 VR 顯示器連線到您的計算機,這只是一個簡單的演示,因此目前這樣就可以了。
啟動和停止 VR 演示
現在我們有了一個 VRDisplay 物件,我們可以用它做很多事情。接下來我們要做的就是連線功能來啟動和停止 WebGL 內容向顯示器的呈現。
繼續上一個程式碼塊,我們現在為啟動/停止按鈕(btn)新增一個事件監聽器 — 當點選此按鈕時,我們想要檢查我們是否已經向顯示器呈現內容(我們以一種相當簡單的方式來做,透過檢查按鈕的 textContent 包含什麼)。
如果顯示器尚未呈現,我們使用 VRDisplay.requestPresent() 方法請求瀏覽器開始向顯示器呈現內容。這需要一個引數,即一個數組,其中包含表示您想要在顯示器中呈現的層的 VRLayerInit 物件。
由於目前您可以顯示的最大層數是 1,並且唯一必需的物件成員是 VRLayerInit.source 屬性(它是您想要在該層中呈現的 <canvas> 的引用;其他引數都提供了合理的預設值 — 請參閱 leftBounds 和 rightBounds),因此引數為 [{ source: canvas }]。
requestPresent() 返回一個 Promise,該 Promise 在演示成功開始時得到 fulfillment。
// Starting the presentation when the button is clicked: It can only be called in response to a user gesture
btn.addEventListener("click", () => {
if (btn.textContent === "Start VR display") {
vrDisplay.requestPresent([{ source: canvas }]).then(() => {
console.log("Presenting to WebVR display");
// ...
});
} else {
// ...
}
});
我們的演示請求成功後,我們現在要開始設定渲染內容以呈現給 VRDisplay。首先,我們將畫布設定為與 VR 顯示區域相同的大小。我們透過使用 VRDisplay.getEyeParameters() 獲取兩隻眼睛的 VREyeParameters 來實現這一點。
然後,我們進行一些簡單的數學計算,根據眼睛的 VREyeParameters.renderWidth 和 VREyeParameters.renderHeight 來計算 VRDisplay 渲染區域的總寬度。
vrDisplay.requestPresent([{ source: canvas }]).then(() => {
// ...
// Set the canvas size to the size of the vrDisplay viewport
const leftEye = vrDisplay.getEyeParameters("left");
const rightEye = vrDisplay.getEyeParameters("right");
canvas.width = Math.max(leftEye.renderWidth, rightEye.renderWidth) * 2;
canvas.height = Math.max(leftEye.renderHeight, rightEye.renderHeight);
// ...
});
接下來,我們 取消之前由 drawScene() 函式內部的 Window.requestAnimationFrame() 呼叫啟動的動畫迴圈,轉而呼叫 drawVRScene()。此函式渲染與之前相同的場景,但有一些特殊的 WebVR 魔法。此內部迴圈由 WebVR 特殊的 VRDisplay.requestAnimationFrame 方法維護。
vrDisplay.requestPresent([{ source: canvas }]).then(() => {
// ...
// stop the normal presentation, and start the vr presentation
window.cancelAnimationFrame(normalSceneFrame);
drawVRScene();
// ...
});
最後,我們更新按鈕文字,以便下次按下時,它將停止向 VR 顯示器呈現。
vrDisplay.requestPresent([{ source: canvas }]).then(() => {
// ...
btn.textContent = "Exit VR display";
});
為了在隨後按下按鈕時停止 VR 呈現,我們呼叫 VRDisplay.exitPresent()。我們還會反轉按鈕的文字內容,並交換 requestAnimationFrame 呼叫。您可以在這裡看到我們正在使用 VRDisplay.cancelAnimationFrame 來停止 VR 渲染迴圈,並透過呼叫 drawScene() 再次啟動正常渲染迴圈。
if (btn.textContent === "Start VR display") {
// ...
} else {
vrDisplay.exitPresent();
console.log("Stopped presenting to WebVR display");
btn.textContent = "Start VR display";
// Stop the VR presentation, and start the normal presentation
vrDisplay.cancelAnimationFrame(vrSceneFrame);
drawScene();
}
一旦演示開始,您將能夠在瀏覽器中看到立體檢視

您將在下面瞭解立體檢視是如何實際產生的。
為什麼 WebVR 有自己的 requestAnimationFrame()?
這是一個好問題。原因是,為了在 VR 顯示器中實現流暢渲染,您需要以顯示器的本機重新整理率而不是計算機的重新整理率來渲染內容。VR 顯示器的重新整理率高於 PC 重新整理率,通常高達 90fps。該速率將與計算機的核心重新整理率不同。
請注意,當 VR 顯示器未呈現時,VRDisplay.requestAnimationFrame 的執行方式與 Window.requestAnimationFrame 完全相同,因此如果您願意,您可以使用單個渲染迴圈,而不是我們應用程式中使用的兩個。我們使用兩個是因為我們希望根據 VR 顯示器是否正在呈現來做一些略微不同的事情,併為了便於理解而將它們分開。
渲染和顯示
此時,我們已經看到了訪問 VR 硬體、請求將我們的場景呈現給硬體以及啟動渲染迴圈所需的所有程式碼。現在讓我們看看渲染迴圈的程式碼,並解釋其 WebVR 特定部分是如何工作的。
首先,我們開始定義渲染迴圈函式 — drawVRScene()。我們在這裡做的第一件事是呼叫 VRDisplay.requestAnimationFrame(),以使迴圈在被呼叫一次後繼續執行(這在我們的程式碼中,當我們開始向 VR 顯示器呈現時發生)。此呼叫被設定為全域性 vrSceneFrame 變數的值,因此我們可以在退出 VR 呈現後,透過呼叫 VRDisplay.cancelAnimationFrame() 來取消迴圈。
function drawVRScene() {
// WebVR: Request the next frame of the animation
vrSceneFrame = vrDisplay.requestAnimationFrame(drawVRScene);
// ...
}
接下來,我們呼叫 VRDisplay.getFrameData(),並將我們想要用於包含幀資料的變數名傳遞給它。我們之前已經初始化了它 — frameData。呼叫完成後,此變數將包含渲染下一幀到 VR 裝置所需的資料,並打包成一個 VRFrameData 物件。它包含諸如用於正確渲染左右眼檢視場景的投影和檢視矩陣,以及當前的 VRPose 物件,其中包含 VR 顯示器的資料,例如方向、位置等。
這必須在每一幀都呼叫,以便渲染檢視始終保持最新。
function drawVRScene() {
// ...
// Populate frameData with the data of the next frame to display
vrDisplay.getFrameData(frameData);
// ...
}
現在我們從 VRFrameData.pose 屬性中檢索當前的 VRPose,儲存位置和方向以供以後使用,如果 poseStatsDisplayed 變數設定為 true,則將當前姿態傳送到姿態統計框進行顯示。
function drawVRScene() {
// ...
// You can get the position, orientation, etc. of the display from the current frame's pose
const curFramePose = frameData.pose;
const curPos = curFramePose.position;
const curOrient = curFramePose.orientation;
if (poseStatsDisplayed) {
displayPoseStats(curFramePose);
}
// ...
}
現在我們清空畫布,然後開始在其上繪圖,以便清楚地看到下一幀,並且我們也不會看到以前渲染的幀
function drawVRScene() {
// ...
// Clear the canvas before we start drawing on it.
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// ...
}
現在我們渲染左眼和右眼的檢視。首先,我們需要建立用於渲染的投影和檢視位置。這些是 WebGLUniformLocation 物件,使用 WebGLRenderingContext.getUniformLocation() 方法建立,並將其作為引數傳遞著色器程式的識別符號和標識名稱。
function drawVRScene() {
// ...
// WebVR: Create the required projection and view matrix locations needed
// for passing into the uniformMatrix4fv methods below
const projectionMatrixLocation = gl.getUniformLocation(
shaderProgram,
"projMatrix",
);
const viewMatrixLocation = gl.getUniformLocation(shaderProgram, "viewMatrix");
// ...
}
下一步渲染涉及
- 使用
WebGLRenderingContext.viewport指定左眼的視口大小 — 這在邏輯上是畫布寬度的一半,以及完整的畫布高度。 - 指定用於渲染左眼的檢視和投影矩陣值 — 這透過
WebGLRenderingContext.uniformMatrix4fv方法完成,該方法傳遞我們上面檢索到的位置值以及從VRFrameData物件獲取的左矩陣。 - 執行
drawGeometry()函式,該函式渲染實際場景 — 由於我們在前兩個步驟中指定的內容,我們將僅為左眼渲染它。
function drawVRScene() {
// ...
// WebVR: Render the left eye's view to the left half of the canvas
gl.viewport(0, 0, canvas.width * 0.5, canvas.height);
gl.uniformMatrix4fv(
projectionMatrixLocation,
false,
frameData.leftProjectionMatrix,
);
gl.uniformMatrix4fv(viewMatrixLocation, false, frameData.leftViewMatrix);
drawGeometry();
// ...
}
現在我們做完全相同的事情,但是針對右眼
function drawVRScene() {
// ...
// WebVR: Render the right eye's view to the right half of the canvas
gl.viewport(canvas.width * 0.5, 0, canvas.width * 0.5, canvas.height);
gl.uniformMatrix4fv(
projectionMatrixLocation,
false,
frameData.rightProjectionMatrix,
);
gl.uniformMatrix4fv(viewMatrixLocation, false, frameData.rightViewMatrix);
drawGeometry();
// ...
}
接下來我們定義 drawGeometry() 函式。大部分只是繪製 3D 立方體所需的通用 WebGL 程式碼。您將在 mvTranslate() 和 mvRotate() 函式呼叫中看到一些 WebVR 特定的部分 — 這些部分將矩陣傳遞給 WebGL 程式,這些矩陣定義了當前幀中立方體的平移和旋轉
您會看到我們正在透過從 VRPose 物件獲取的 VR 顯示器的位置 (curPos) 和方向 (curOrient) 來修改這些值。結果是,例如,當您將頭部向左移動或旋轉時,x 位置值 (curPos[0]) 和 y 旋轉值 ([curOrient[1]) 會新增到 x 平移值中,這意味著立方體將向右移動,正如您在觀察某物並向左移動/轉動頭部時所期望的那樣。
這是一種快速而粗糙地使用 VR 姿態資料的方法,但它說明了基本原理。
function drawGeometry() {
// Establish the perspective with which we want to view the
// scene. Our field of view is 45 degrees, with a width/height
// ratio of 640:480, and we only want to see objects between 0.1 units
// and 100 units away from the camera.
perspectiveMatrix = makePerspective(45, 640.0 / 480.0, 0.1, 100.0);
// Set the drawing position to the "identity" point, which is
// the center of the scene.
loadIdentity();
// Now move the drawing position a bit to where we want to start
// drawing the cube.
mvTranslate([
0.0 - curPos[0] * 25 + curOrient[1] * 25,
5.0 - curPos[1] * 25 - curOrient[0] * 25,
-15.0 - curPos[2] * 25,
]);
// Save the current matrix, then rotate before we draw.
mvPushMatrix();
mvRotate(cubeRotation, [0.25, 0, 0.25 - curOrient[2] * 0.5]);
// Draw the cube by binding the array buffer to the cube's vertices
// array, setting attributes, and pushing it to GL.
gl.bindBuffer(gl.ARRAY_BUFFER, cubeVerticesBuffer);
gl.vertexAttribPointer(vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0);
// Set the texture coordinates attribute for the vertices.
gl.bindBuffer(gl.ARRAY_BUFFER, cubeVerticesTextureCoordBuffer);
gl.vertexAttribPointer(textureCoordAttribute, 2, gl.FLOAT, false, 0, 0);
// Specify the texture to map onto the faces.
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, cubeTexture);
gl.uniform1i(gl.getUniformLocation(shaderProgram, "uSampler"), 0);
// Draw the cube.
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVerticesIndexBuffer);
setMatrixUniforms();
gl.drawElements(gl.TRIANGLES, 36, gl.UNSIGNED_SHORT, 0);
// Restore the original matrix
mvPopMatrix();
}
程式碼的下一部分與 WebVR 無關 — 它只是更新每幀立方體的旋轉
function drawVRScene() {
// ...
// Update the rotation for the next draw, if it's time to do so.
let currentTime = new Date().getTime();
if (lastCubeUpdateTime) {
const delta = currentTime - lastCubeUpdateTime;
cubeRotation += (30 * delta) / 1000.0;
}
lastCubeUpdateTime = currentTime;
// ...
}
渲染迴圈的最後一部分是呼叫 VRDisplay.submitFrame() — 現在所有工作都已完成,我們已經在 <canvas> 上渲染了顯示,此方法隨後將幀提交到 VR 顯示器,以便也在那裡顯示。
function drawVRScene() {
// ...
// WebVR: Indicate that we are ready to present the rendered frame to the VR display
vrDisplay.submitFrame();
}
顯示姿態(位置、方向等)資料
在本節中,我們將討論 displayPoseStats() 函式,它在每一幀顯示我們更新的姿態資料。該函式相當簡單。
首先,我們將從 VRPose 物件獲取的六個不同屬性值儲存在各自的變數中 — 每個變數都是一個 Float32Array。
function displayPoseStats(pose) {
const pos = pose.position;
const orient = pose.orientation;
const linVel = pose.linearVelocity;
const linAcc = pose.linearAcceleration;
const angVel = pose.angularVelocity;
const angAcc = pose.angularAcceleration;
// ...
}
然後我們將資料寫入資訊框,並在每一幀更新它。我們使用 toFixed() 將每個值限制為三位小數,否則這些值很難閱讀。
您應該注意,我們使用了條件表示式來檢測線性加速度和角加速度陣列是否成功返回,然後才顯示資料。大多數 VR 硬體目前尚未報告這些值,因此如果我們不這樣做,程式碼會丟擲錯誤(如果未成功報告,陣列將返回 null)。
function displayPoseStats(pose) {
// ...
posStats.textContent =
`Position: ` +
`x ${pos[0].toFixed(3)}, ` +
`y ${pos[1].toFixed(3)}, ` +
`z ${pos[2].toFixed(3)}`;
orientStats.textContent =
`Orientation: ` +
`x ${orient[0].toFixed(3)}, ` +
`y ${orient[1].toFixed(3)}, ` +
`z ${orient[2].toFixed(3)}`;
linVelStats.textContent =
`Linear velocity: ` +
`x ${linVel[0].toFixed(3)}, ` +
`y ${linVel[1].toFixed(3)}, ` +
`z ${linVel[2].toFixed(3)}`;
angVelStats.textContent =
`Angular velocity: ` +
`x ${angVel[0].toFixed(3)}, ` +
`y ${angVel[1].toFixed(3)}, ` +
`z ${angVel[2].toFixed(3)}`;
if (linAcc) {
linAccStats.textContent =
`Linear acceleration: ` +
`x ${linAcc[0].toFixed(3)}, ` +
`y ${linAcc[1].toFixed(3)}, ` +
`z ${linAcc[2].toFixed(3)}`;
} else {
linAccStats.textContent = "Linear acceleration not reported";
}
if (angAcc) {
angAccStats.textContent =
`Angular acceleration: ` +
`x ${angAcc[0].toFixed(3)}, ` +
`y ${angAcc[1].toFixed(3)}, ` +
`z ${angAcc[2].toFixed(3)}`;
} else {
angAccStats.textContent = "Angular acceleration not reported";
}
}
WebVR 事件
WebVR 規範包含許多被觸發的事件,允許我們的應用程式程式碼對 VR 顯示器的狀態變化做出反應(請參閱視窗事件)。例如
vrdisplaypresentchange— 當 VR 顯示器的呈現狀態發生變化時觸發 — 即,從呈現變為不呈現,反之亦然。vrdisplayconnect— 當相容的 VR 顯示器連線到計算機時觸發。vrdisplaydisconnect— 當相容的 VR 顯示器從計算機斷開連線時觸發。
為了演示它們的工作原理,我們的簡單演示包括以下示例
window.addEventListener("vrdisplaypresentchange", (e) => {
console.log(
`Display ${e.display.displayId} presentation has changed. Reason given: ${e.reason}.`,
);
});
如您所見,VRDisplayEvent 物件提供了兩個有用的屬性 — VRDisplayEvent.display,它包含對事件響應的 VRDisplay 的引用,以及 VRDisplayEvent.reason,它包含事件觸發的人類可讀原因。
這是一個非常有用的事件;您可以使用它來處理顯示器意外斷開連線的情況,防止丟擲錯誤並確保使用者瞭解情況。在 Google 的 webvr.info 演示中,該事件用於執行 onVRPresentChange() 函式,該函式會適當地更新 UI 控制元件並調整畫布大小。
總結
本文為您提供了建立簡單 WebVR 1.1 應用程式的最基本知識,以幫助您入門。