移動、方向和運動:一個 WebXR 示例
在本文中,我們將利用我們 WebXR 系列教程前幾篇文章中介紹的資訊來構建一個示例,該示例動畫化一個旋轉的立方體,使用者可以使用 VR 頭戴裝置、鍵盤和/或滑鼠圍繞該立方體自由移動。這將有助於鞏固您對 3D 圖形和 VR 幾何原理的理解,並有助於確保您理解 XR 渲染過程中使用的函式和資料如何協同工作。
圖:該示例的實際執行截圖 
此示例的核心——旋轉、有紋理、有光照的立方體——取自我們的 WebGL 教程系列;確切地說,是該系列倒數第二篇文章,涵蓋了 WebGL 中的光照。
在閱讀本文及隨附的原始碼時,請記住 3D 頭戴裝置的顯示屏是單個螢幕,一分為二。螢幕的左半部分僅供左眼觀看,而右半部分僅供右眼觀看。為了沉浸式呈現而渲染場景需要對場景進行多次渲染——從每隻眼睛的角度各渲染一次。
當渲染左眼時,XRWebGLLayer 的 視口 配置為將繪圖限制在繪圖表面的左半部分。相反,當渲染右眼時,視口設定為將繪圖限制在表面的右半部分。
此示例透過在螢幕上顯示畫布來演示這一點,即使使用 XR 裝置以沉浸式顯示方式呈現場景也是如此。
依賴項
雖然我們不會為此示例依賴任何 3D 圖形框架,例如 three.js 等,但我們確實使用 glMatrix 庫進行矩陣數學運算,我們過去在其他示例中也使用過它。此示例還匯入了由沉浸式 Web 工作組(負責 WebXR API 規範的團隊)維護的 WebXR polyfill。透過匯入此 polyfill,我們允許該示例在許多尚未實現 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 = "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 polyfill。如果為false,則僅當瀏覽器未實現navigator.xr時才使用 polyfill。 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;
const xrButton = document.querySelector("#enter-xr");
const projectionMatrixOut = document.querySelector("#projection-matrix div");
const modelMatrixOut = document.querySelector("#model-view-matrix div");
const cameraMatrixOut = document.querySelector("#camera-matrix div");
const mouseMatrixOut = document.querySelector("#mouse-matrix div");
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
xrButton.addEventListener("click", onXRButtonClick);
if (!navigator.xr || enableForcePolyfill) {
console.log("Using the polyfill");
polyfill = new WebXRPolyfill();
}
setupXRButton();
我們添加了一個 click 事件的處理程式。然後我們檢查 navigator.xr 是否已定義。如果未定義——並且/或 enableForcePolyfill 配置常量設定為 true——我們透過例項化 WebXRPolyfill 類來安裝 WebXR polyfill。
處理啟動和關閉 UI
然後我們呼叫 setupXRButton() 函式,該函式根據 SESSION_TYPE 常量中指定的會話型別對 WebXR 支援的可用性來配置“進入/退出 WebXR”按鈕,以便根據需要啟用或停用它。
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 會話型別的 WebXR 會話,然後呼叫 sessionStarted() 以在該 WebXR 會話中開始執行場景。
另一方面,如果我們已經有一個正在進行的會話,我們呼叫其 end() 方法來停止會話。
我們在這段程式碼中做的最後一件事是檢查 xrSession 是否仍然不是 NULL。如果是,我們呼叫 sessionEnded(),這是 end 事件的處理程式。這段程式碼應該是不必要的,但似乎存在一個問題,即至少某些瀏覽器沒有正確觸發 end 事件。透過直接執行事件處理程式,我們在此情況下手動完成關閉過程。
啟動 WebXR 會話
sessionStarted() 函式透過設定事件處理程式、編譯和安裝頂點和片段著色器的 GLSL 程式碼,以及在啟動渲染迴圈之前將 WebGL 層附加到 WebXR 會話來實際設定和啟動會話。它作為 requestSession() 返回的 Promise 的處理程式被呼叫。
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://mdn.github.io/shared-assets/images/examples/fx-nightly-512.png",
);
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 結束時我們就會收到通知。
然後我們獲取對 HTML 中找到的 <canvas> 元素的引用——以及它的 WebGL 渲染上下文——這將用作場景的繪圖表面。在元素上呼叫 getContext() 時請求 xrCompatible 屬性以訪問畫布的 WebGL 渲染上下文。這確保了上下文已配置為用作 WebXR 渲染的源。
接下來,我們為 mousemove 和 contextmenu 新增事件處理程式,但僅當 allowMouseRotation 常量為 true 時。mousemove 處理程式將根據滑鼠的移動處理檢視的俯仰和偏航。由於“旋轉”功能僅在按下滑鼠右鍵時才起作用,並且單擊滑鼠右鍵會觸發上下文選單,因此我們為畫布添加了一個 contextmenu 事件處理程式,以防止使用者最初開始拖動滑鼠時出現上下文選單。
接下來,我們編譯著色器程式;獲取對其變數的引用;初始化儲存每個位置陣列的緩衝區;每個頂點的位置表中的索引;頂點法線;以及每個頂點的紋理座標。這都直接取自 WebGL 示例程式碼,因此請參閱 WebGL 中的光照 及其前面的文章 使用 WebGL 建立 3D 物件 和 在 WebGL 中使用紋理。然後呼叫我們的 loadTexture() 函式來載入紋理檔案。
現在渲染結構和資料已載入,我們開始準備執行 XRSession。我們透過呼叫 XRSession.updateRenderState() 並將 baseLayer 設定為新的 XRWebGLLayer,將會話連線到 WebGL 層,以便它知道要使用什麼作為渲染表面。
然後我們檢視 SESSION_TYPE 常量的值,以確定 WebXR 上下文應該是沉浸式還是內聯式。沉浸式會話使用 local 參考空間,而內聯會話使用 viewer 參考空間。
glMatrix 庫的 4x4 矩陣 fromTranslation() 函式用於將 viewerStartPosition 常量中給定的檢視器起始位置轉換為變換矩陣 cubeMatrix。檢視器的起始方向 viewerStartOrientation 常量被複制到 cubeOrientation 中,後者將用於跟蹤立方體隨時間的旋轉。
sessionStarted() 透過呼叫會話的 requestReferenceSpace() 方法來獲取描述建立物件所處空間的參考空間物件。當返回的 Promise 解析為 XRReferenceSpace 物件時,我們呼叫其 getOffsetReferenceSpace 方法以獲取表示物件座標系的參考空間物件。新空間的起點位於 viewerStartPosition 指定的世界座標,其方向設定為 cubeOrientation。然後我們透過呼叫會話的 requestAnimationFrame() 方法讓會話知道我們已準備好繪製一幀。我們記錄返回的請求 ID,以防以後需要取消請求。
最後,sessionStarted() 返回表示使用者 WebXR 會話的 XRSession。
會話結束時
當 WebXR 會話結束時——無論是由於使用者關閉還是透過呼叫 XRSession.end()——都會發送 end 事件;我們已將其設定為呼叫名為 sessionEnded() 的函式。
function sessionEnded() {
xrButton.innerText = "Enter WebXR";
if (animationFrameRequestID) {
xrSession.cancelAnimationFrame(animationFrameRequestID);
animationFrameRequestID = 0;
}
xrSession = null;
}
如果我們希望以程式設計方式結束 WebXR 會話,我們也可以直接呼叫 sessionEnded()。無論哪種情況,按鈕的標籤都會更新,以指示單擊將開始一個會話,然後,如果存在動畫幀的待處理請求,我們會透過呼叫 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 裝置,我們清除幀為黑色,並準備開始渲染。
自上一幀渲染以來經過的時間(以秒為單位)透過從當前時間(由 time 引數指定)減去上一幀的時間戳 lastFrameTime,然後乘以 0.001 將毫秒轉換為秒來計算。然後將當前時間儲存到 lastFrameTime 中;
drawFrame() 函式透過迭代 XRViewerPose 中找到的每個檢視,為檢視設定視口,並呼叫 renderScene() 來渲染幀。透過為每個檢視設定視口,我們處理了每個眼睛的檢視都渲染到 WebGL 幀的一半的典型場景。XR 硬體然後處理確保每隻眼睛只看到該影像中為其意圖的部分。
注意:在此示例中,我們同時在 XR 裝置和螢幕上視覺呈現幀。為了確保螢幕上的畫布具有正確的尺寸以允許我們執行此操作,我們將其寬度設定為等於單個 XRView 寬度乘以檢視數;畫布高度始終與視口的高度相同。在常規 WebXR 渲染迴圈中不需要調整畫布尺寸的兩行程式碼。
應用使用者輸入
applyViewerControls() 函式在開始渲染任何內容之前由 drawFrame() 呼叫,它根據 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() 函式來實際渲染使用者當前可見的世界部分。它為每隻眼睛呼叫一次,每隻眼睛的位置略有不同,以便建立 XR 裝置所需的 3D 效果。
這段程式碼的大部分是典型的 WebGL 渲染程式碼,直接取自 WebGL 中的光照 一文中的 drawScene() 函式,您應該在那裡查詢有關此示例的 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 紋理。如果影像的尺寸都不是 2 的冪(參見
isPowerOf2()函式),則停用 mipmapping 並將環繞限制在邊緣。這是因為 mipmapped 紋理的最佳化渲染僅適用於 WebGL 1 中尺寸為 2 的冪的紋理。WebGL 2 支援任意大小的紋理進行 mipmapping。 isPowerOf2()-
如果指定的值是 2 的冪,則返回
true;否則返回false。
整合起來
當你將程式碼與 HTML 和一些額外的 JavaScript 結合起來時,你就會得到類似我們的 WebXR:帶旋轉物件和使用者移動的示例 演示。記住:當你四處遊蕩時,如果迷路了,只需按下 R 鍵即可將自己重置到起點。
一個小提示:如果您沒有 XR 裝置,您可以嘗試將臉部非常靠近螢幕,鼻子居中於畫布中左右眼影像之間的邊界線,這樣也許可以獲得一些 3D 效果。透過仔細地透過螢幕聚焦影像,並緩慢地向前和向後移動,您最終應該能夠使 3D 影像對焦。這可能需要練習,並且您的鼻子可能真的會碰到螢幕,具體取決於您的視力敏銳程度。
以這個示例作為起點,您可以做很多事情。嘗試向世界新增更多物件,或者改進移動控制以使其更逼真。新增牆壁、天花板和地板,將您封閉在一個空間中,而不是在一個看似無限的宇宙中迷失。新增碰撞檢測或命中檢測,或更改立方體每個面的紋理的能力。
只要你下定決心,幾乎沒有什麼限制。
另見
- 學習 WebGL(包含一些關於相機及其與虛擬世界關係的精彩視覺化)
- WebGL 基礎知識
- 學習 OpenGL