視覺化學習:WebGL實現縮放平移

beckyye發表於2024-03-21

前言

在上篇文章中,我們使用WebGL實現了網格背景,當時有提到說使用WebGL來實現的好處之一,是網格背景可以與畫布上的其他元素更好地融合,比如一起縮放平移,那麼在WebGL中怎麼實現縮放和平移呢?現在我們已經實現了網格背景,接下來我們就用網格背景作為例子來了解一下WebGL中的縮放和平移。

這裡需要用到我之前學過的變換矩陣,不熟悉變換矩陣的小夥伴可以看我之前的文章《CSS transform與仿射變換》,或者查閱其他相關資料;簡單來說就是使用矩陣來表示頂點座標的變換。

具體實現

接下來我們在程式碼中來具體實現這個效果。

1. 改造頂點著色器

首先我們先改造頂點著色器的GLSL程式碼,對頂點應用變換矩陣。

attribute vec2 a_vertexPosition;
attribute vec2 uv;

varying vec2 vUv;

uniform int scale;
uniform vec2 offset;

mat3 translateMatrix = mat3( // 平移矩陣
  1.0, 0.0, 0.0, // 第一列
  0.0, 1.0, 0.0, // 第二列
  offset.x, offset.y, 1.0 // 第三列
);

mat3 scaleMatrix = mat3( // 縮放矩陣
  float(scale), 0.0, 0.0,
  0.0, float(scale), 0.0,
  0.0, 0.0, 1.0
);

void main() {
  gl_PointSize = 1.0;
  vUv = uv;
  vec3 pos = scaleMatrix * translateMatrix * vec3(a_vertexPosition, 1.0);
  gl_Position = vec4(pos, 1.0);
}

程式碼中我們增加接收兩個uniform常量scaleoffset,表示縮放比例和平移的偏移量;並且定義兩個矩陣:平移矩陣translateMatrix和縮放矩陣scaleMatrix。需要注意GLSL這裡矩陣的寫法是列主序的,也就是說這裡的第一行其實是平常我們認為的第一列,也就是說假如有平移矩陣乘以原座標:translateMatrix * vec3(a_vertexPosition, 1.0),我們會得到如下結果:

\[\begin{bmatrix} 1 & 0 & offset.x \\ 0 & 1 & offset.y \\ 0 & 0 & 1 \end{bmatrix} \times \begin{bmatrix} x_0 \\ y_0 \\ 1 \end{bmatrix} = \begin{bmatrix} x_0 + offset.x\\ y_0 + offset.y \\ 1 \end{bmatrix} \]

將兩個矩陣相乘表示疊加效果,再與座標向量相乘後,我們就能得到對映的新頂點座標。

2. 改造js程式碼

頂點著色器中增加接收的兩個引數需要js程式碼傳遞過去,接下來我們就對js程式碼進行改造。

首先是設定這兩個變數的初始值,scale等於1表示原始大小,offset設定的初始偏移量為0,也就是不偏移。

// ...
renderer.uniforms.scale = 1;
renderer.uniforms.offset = [0.0, 0.0];
// ...

然後我們新增對滑鼠滾輪事件以及滑鼠按下和放開事件的監聽。

// ...
const addEvent = () => {
  patternPracticeRef.value.addEventListener('mousewheel', wheelEventHandler);
  patternPracticeRef.value.addEventListener('mouseup', mouseUpHandler);
  patternPracticeRef.value.addEventListener('mousedown', mouseDownHandler);
}
addEvent();
// ...

接下來我們就來完成這三個事件監聽的回撥函式。

我們先來完成滾輪事件的監聽回撥,這個比較簡單。e.wheelDeltaY > 0表示滾輪向下滾動,實現放大效果,這裡我使用了整數表示放大倍數,這是因為之前我有使用浮點數來實現過,但是存在一些問題,比如繪製出來的網格線會有可能粗細不同,甚至會缺少一些線,這估計是由於精度原因導致的,我還沒去進一步的研究。

const wheelEventHandler = e => {
  e.preventDefault();
  if (e.wheelDeltaY > 0) { // 向下滾,放大
    if (renderer.uniforms.scale <= 50) {
      renderer.uniforms.scale += 1;
    }
  } else { // 向上滾,縮小
    if (renderer.uniforms.scale > 1) {
      renderer.uniforms.scale -= 1;
    }
  }
}
const mouseDownHandler = e => {
  // ...
}
const mouseUpHandler = e => {
	// ...
}

此時我們在頁面上進行測試一下,就可以看到縮放的效果實現了。
image

接著我們來實現平移的效果,因為偏移量需要根據滑鼠的初始位置和移動結束的位置計算得到,所以我們增加一個物件型別的變數lastPos來記錄滑鼠的初始位置,並在滑鼠按下的事件回撥中對它賦值。

// ...
const lastPos = {};
// ...
const mouseDownHandler = e => {
  e.preventDefault();
  // 記錄初始位置
  lastPos.x = e.offsetX;
  lastPos.y = e.offsetY;
  // 繫結事件
  patternPracticeRef.value.addEventListener('mousemove', mouseMoveHandler);
}

在滑鼠按下後,我們對滑鼠的移動事件開啟監聽,便於實時更新位置資訊。

const mouseMoveHandler = e => {
  e.preventDefault();
  // 計算座標差值並轉換為Canvas差值
  const { offsetX: x, offsetY: y } = e;
  const translateX = (x - lastPos.x) / patternPracticeRef.value.width;
  const translateY = (lastPos.y - y) / patternPracticeRef.value.height;
  // 設定偏移量
  renderer.uniforms.offset = [translateX, translateY];
}

因為在頁面上y軸向下,與WebGL中y軸向上是相反的,所以這裡計算y方向的偏移量使用lastPos.y - y。我們透過除以canvas的寬高,就能轉換得到偏移量在WebGL裡的對應數值。

接著我們在滑鼠按鍵鬆開事件監聽的回撥中,對移動監聽進行移除。

const mouseDownHandler = e => {
   e.preventDefault();
  // 解綁事件
  patternPracticeRef.value.addEventListener('mousemove', mouseMoveHandler);
}

此時在頁面上測試,能看到我們可以對網格進行拖動平移了,但很明顯這其中還存在問題,當我們完成第一次平移後,想再點選滑鼠來平移,就會發現在按下滑鼠的那一刻網格會發生閃爍,這是因為在上一次平移完成後,圖片的中心點已經改變了,而在GLSL程式碼中的偏移矩陣中,偏移量的設定是相對(0, 0)點的,所以會閃爍一下,也就是說我們需要考慮中心點的影響,在第二次平移時要在新的中心點的基礎上去進行平移,在這裡我使用一個lastCenter變數來儲存中心點的位置。

const lastPos = {}, lastCenter = {};

並在滑鼠按鍵鬆開的事件回撥中對畫布中心點的資訊進行更新。

const mouseUpHandler = e => {
  e.preventDefault();
  // 計算座標差值並轉換為Canvas差值
  const { offsetX: x, offsetY: y } = e;
  const translateX = (x - lastPos.x) / patternPracticeRef.value.width;
  const translateY = (lastPos.y - y) / patternPracticeRef.value.height;
  // 更新新的中心點
  lastCenter.x = translateX + lastCenter.x;
  lastCenter.y = translateY + lastCenter.y;
  // 解除事件繫結
  patternPracticeRef.value.removeEventListener('mousemove', mouseMoveHandler);
}

同時在滑鼠移動過程中偏移量的更新我們也要把中心點的座標考慮進去。

const mouseMoveHandler = e => {
  e.preventDefault();
  // 計算座標差值並轉換為Canvas差值
  const { offsetX: x, offsetY: y } = e;
  const translateX = (x - lastPos.x) / patternPracticeRef.value.width;
  const translateY = (lastPos.y - y) / patternPracticeRef.value.height;
  // 設定偏移量
  renderer.uniforms.offset = [translateX + lastCenter.x, translateY + lastCenter.y];
}

這個時候我們再去測試一下,可以看到就是正常的了。

總結

到這裡為止我們就實現了在WebGL中的縮放和平移,這裡主要就是藉助了滑鼠事件的監聽和變換矩陣的應用。

完整程式碼
最終效果

相關文章