用 WebGL 作一個齒輪動畫

原文:Aral Roca

翻譯:瘋狂的技術宅前端

https://aralroca.com/blog/how...react

未經容許嚴禁轉載git

本文繼續 「WebGL 的第一步」 中的內容,上一篇文章中我講述了 WebGL是什麼以及它是如何工做的,包括:shader、program、緩衝區、如何將數據從 CPU 連接到 GPU 和最終怎樣渲染三角形。程序員

在本文中,咱們將研究如何渲染更復雜的結構以及怎樣使其運動。因此,咱們將實現三個動態齒輪github

本文中產生的齒輪

識別形狀

要繪製的齒輪由組成,不過這些圓須要一些變化:帶齒的圓、帶有彩色邊框的圓和填充有顏色的圓。web

image.png

咱們能夠經過繪製圓來繪製這些齒輪,可是在 WebGL 中只能光柵化三角形,點和線...因此這些圓之間的區別是什麼,怎樣才能作到呢?面試

帶邊框的圓

咱們將使用多個來繪製帶邊框的圓,:canvas

用點繪製圓

填充顏色的圓

咱們將使用多個三角形繪製一個填充顏色的圓,:segmentfault

用退化三角形繪製的實心圓

因此須要用退化三角形(Triangle strip)繪製模式:數組

退化三角形(Triangle strip) 是三角形網格中一系列相連的三角形,共享頂點,從而能夠更有效地利用計算機圖形的內存。它們比不帶索引的三角列表更有效,但效率通常不如帶索引的三角列表穩定。之因此使用退化三角形,主要緣由是可以減小建立一系列三角形所需的數據量。存儲在內存中的頂點數量從 3N 減小到了 N + 2,其中 N 是要繪製的三角形數量。這樣能夠減小磁盤空間的使用,並可以使它們更快地加載到內存中。

帶齒輪的圓

咱們還會使用三角形處理齒輪。此次不用「strip」模式,而是要繪製從圓周中心輻射開的三角形。

齒輪齒爲三角形

在構建齒輪時,還要在內部建立另一個充滿顏色的圓,以便使齒輪從圓自己突出出來。

識別要繪製的數據

這3種圖形的共同點是能夠從 2 個變量中計算出它們的座標:

  1. 圓心(xy
  2. 半徑

在上一篇文章中咱們知道了,webGL 中的座標範圍是從 -1 到 1。先讓找到每一個齒輪的中心及其半徑:

image.png

此外還有一些特定數字的可選變量,例如:

  • 齒數
  • 筆觸顏色(邊框的顏色)*
  • 填充色
  • 子級(更多具備相同數據結構的齒輪)
  • 旋轉方向(僅對父級有效)

最後在 JavaScript 中,咱們將獲得一個包含三個齒輪及其全部零件的數據的數組:

const x1 = 0.1
const y1 = -0.2

const x2 = -0.42
const y2 = 0.41

const x3 = 0.56
const y3 = 0.28

export const gears = [
  {
    center: [x1, y1],
    direction: 'counterclockwise',
    numberOfTeeth: 20,
    radius: 0.45,
    fillColor: [0.878, 0.878, 0.878],
    children: [
      {
        center: [x1, y1],
        radius: 0.4,
        strokeColor: [0.682, 0.682, 0.682],
      },
      {
        center: [x1, y1],
        radius: 0.07,
        fillColor: [0.741, 0.741, 0.741],
        strokeColor: [0.682, 0.682, 0.682],
      },
      {
        center: [x1 - 0.23, y1],
        radius: 0.12,
        fillColor: [1, 1, 1],
        strokeColor: [0.682, 0.682, 0.682],
      },
      {
        center: [x1, y1 - 0.23],
        radius: 0.12,
        fillColor: [1, 1, 1],
        strokeColor: [0.682, 0.682, 0.682],
      },
      {
        center: [x1 + 0.23, y1],
        radius: 0.12,
        fillColor: [1, 1, 1],
        strokeColor: [0.682, 0.682, 0.682],
      },
      {
        center: [x1, y1 + 0.23],
        radius: 0.12,
        fillColor: [1, 1, 1],
        strokeColor: [0.682, 0.682, 0.682],
      },
    ],
  },
  {
    center: [x2, y2],
    direction: 'clockwise',
    numberOfTeeth: 12,
    radius: 0.3,
    fillColor: [0.741, 0.741, 0.741],
    children: [
      {
        center: [x2, y2],
        radius: 0.25,
        strokeColor: [0.682, 0.682, 0.682],
      },
      {
        center: [x2, y2],
        radius: 0.1,
        fillColor: [0.682, 0.682, 0.682],
        strokeColor: [0.6, 0.6, 0.6],
      },
    ],
  },
  {
    center: [x3, y3],
    direction: 'clockwise',
    numberOfTeeth: 6,
    radius: 0.15,
    fillColor: [0.741, 0.741, 0.741],
    children: [
      {
        center: [x3, y3],
        radius: 0.1,
        strokeColor: [0.682, 0.682, 0.682],
      },
      {
        center: [x3, y3],
        radius: 0.02,
        fillColor: [0.682, 0.682, 0.682],
        strokeColor: [0.6, 0.6, 0.6],
      },
    ],
  },
]

對於顏色,有一點須要注意:取值範圍是從 0 到 1,而不是從 0 到 255,或從 0 到 F,這些是咱們在 CSS 中慣用的。例如,[0.682,0.682,0.682] 等同於 rgb(174,174,174)#AEAEAE

怎樣實現旋轉

在開始實現以前須要知道如何實現每一個齒輪的旋轉。

爲了瞭解旋轉和其餘線性變換,我強烈建議你看看3blue1brown的線性代數視頻課程,該視頻很好地說明了這一點:

(視頻4)

總而言之,若是將位置乘以任何矩陣,都將會獲得一個轉換。咱們必須將每一個齒輪位置乘以旋轉矩陣。須要在其前面添加每一個「轉換」。若是要旋轉,咱們將執行 rotation * positions 而不是 positions * rotation

能夠經過知道弧度角來建立旋轉矩陣:

function rotation(angleInRadians = 0) {
  const c = Math.cos(angleInRadians)
  const s = Math.sin(angleInRadians)

  return [
    c, -s, 0, 
    s, c, 0, 
    0, 0, 1
  ]
}

這樣就能夠經過將每一個齒輪的位置與其各自的旋轉矩陣相乘來使每一個齒輪不一樣地旋轉。爲了產生真實的旋轉效果,在每一個幀中必須稍微增長角度,直到完成完整的旋轉,而且角度轉回到0。

可是僅僅將位置與該矩陣相乘是不夠的。若是這樣作,你將會看到下面這樣的結果:

rotationMatrix * positionMatrix // 這不是咱們想要的

在canvas上旋轉的齒輪(不是咱們想要的)

咱們已經使齒輪旋轉了,可是旋轉軸倒是畫布的中心,這是錯誤的。咱們但願他們圍繞本身的中心旋轉。

爲了解決這個問題,首先把使用名爲 translate 的轉換將齒輪移動到畫布的中心。而後,再把應用正確的旋轉(該軸將再次成爲畫布的中心,但在這種狀況下,它也是齒輪的中心),最後把齒輪移回其原始位置(再次使用 translate)。

轉換矩陣定義以下:

function translation(tx, ty) {
  return [
    1, 0, 0, 
    0, 1, 0, 
    tx, ty, 1
  ]
}

咱們將建立兩個轉換矩陣:translation(centerX, centerY)translation(-centerX, -centerY)。它們的中心必須是每一個齒輪的中心。

因此要執行下面的矩陣乘法:

// 如今它們會圍繞本身的軸心旋轉
translationMatrix * rotationMatrix * translationToOriginMatrix * positionMatrix

正確的旋轉方式

你可能想知道如何使每一個齒輪按照本身的速度旋轉。

有一個簡單的公式能夠根據齒數計算速度:

(Speed A * Number of teeth A) = (Speed B * Number of teeth B)

這樣,在每一個框架中,咱們能夠爲每一個齒輪增長一個不一樣的角度步長,而且每一個齒輪都以他們應有的速度旋轉。

實現

你看到這裏應該知道:

  • 應該畫什麼,怎樣畫。
  • 咱們有每一個齒輪及其零件的座標。
  • 怎樣旋轉每一個齒輪。

下面看看如何用 JavaScript 和 GLSL 實現。

用着色器初始化程序

編寫 vertex shader 來計算頂點的位置:

const vertexShader = `#version 300 es
precision mediump float;
in vec2 position;
uniform mat3 u_rotation;
uniform mat3 u_translation;
uniform mat3 u_moveOrigin;

void main () {
  vec2 movedPosition = (u_translation * u_rotation * u_moveOrigin * vec3(position, 1)).xy;
  gl_Position = vec4(movedPosition, 0.0, 1.0);
  gl_PointSize = 1.0;
}
`

與上一篇文章中使用的頂點着色器不一樣,咱們將傳遞 u_translationu_rotationu_moveOrigin 矩陣,所以 gl_Position 是四個矩陣的乘積(還有 position) 。像上一節所所說的那樣,經過這種方式產生旋轉。另外,咱們將使用 gl_PointSize 定義所繪製的每一個點的大小(這對於帶有邊框的圓頗有用)。

注意:咱們能夠直接用 JavaScript 在 CPU 上執行矩陣乘法的操做,而且已經在這裏傳遞了最終矩陣,但實際上 GPU 纔是專門爲矩陣運算而設計的,由於這樣作的性能要好得多。另外因爲沒法直接對數組進行乘法運算,因此在 JavaScript 中須要一個輔助函數來進行乘法運算。

下面編寫片斷着色器來計算與每一個位置對應的像素顏色:

const fragmentShader = `#version 300 es
precision mediump float;
out vec4 color;
uniform vec3 inputColor;

void main () {
   color = vec4(inputColor, 1.0);
}
`

給出用 JavaScript 在 CPU 中所定義的顏色,並將其傳遞給 GPU 來對圖形進行着色。

如今可使用着色器建立程序,經過添加線條來獲取咱們在頂點着色器中定義的統一位置。這樣稍後在運行腳本時,能夠將每一個矩陣發送到每一幀的每一個統一位置。

const gl = getGLContext(canvas)
const vs = getShader(gl, vertexShader, gl.VERTEX_SHADER)
const fs = getShader(gl, fragmentShader, gl.FRAGMENT_SHADER)
const program = getProgram(gl, vs, fs)
const rotationLocation = gl.getUniformLocation(program, 'u_rotation')
const translationLocation = gl.getUniformLocation(program, 'u_translation')
const moveOriginLocation = gl.getUniformLocation(program, 'u_moveOrigin')

run() // 下一節解釋這個函數

getGLContextgetShadergetProgram 完成了咱們在上一篇文章中的操做。我把它們放在這裏:

function getGLContext(canvas, bgColor) {
  const gl = canvas.getContext('webgl2')
  const defaultBgColor = [1, 1, 1, 1]

  gl.clearColor(...(bgColor || defaultBgColor))
  gl.clear(gl.DEPTH_BUFFER_BIT | gl.COLOR_BUFFER_BIT)

  return gl
}

function getShader(gl, shaderSource, shaderType) {
  const shader = gl.createShader(shaderType)

  gl.shaderSource(shader, shaderSource)
  gl.compileShader(shader)

  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    console.error(gl.getShaderInfoLog(shader))
  }

  return shader
}

function getProgram(gl, vs, fs) {
  const program = gl.createProgram()

  gl.attachShader(program, vs)
  gl.attachShader(program, fs)
  gl.linkProgram(program)
  gl.useProgram(program)

  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
    console.error(gl.getProgramInfoLog(program))
  }

  return program
}

繪製每幀 + 計算旋轉角度

上一節代碼中的 run 函數負責在每一幀中以不一樣角度繪製齒輪。

// 1 個齒的齒輪步長,
// 齒數更多的步長將用如下公式計算:
// realRotationStep = rotationStep / numberOfTeeth
const rotationStep = 0.2

// 角度都初始化爲0
const angles = Array.from({ length: gears.length }).map((v) => 0)

function run() {
  // 爲每一個齒輪計算在該幀的角度
  gears.forEach((gear, index) => {
    const direction = gear.direction === 'clockwise' ? 1 : -1
    const step = direction * (rotationStep / gear.numberOfTeeth)

    angles[index] = (angles[index] + step) % 360
  })

  drawGears() // 下一節解釋這個函數

  // Render next frame
  window.requestAnimationFrame(run)
}

根據齒輪組數組中的數據,能夠知道「齒」的數量以及每一個齒輪的旋轉方向。這樣就能夠計算每幀中每一個齒輪的角度。保存新的計算角度後調用函數 drawGears 來正確的角度繪製每一個齒輪。而後遞歸地再次調用 run 函數(與window.requestAnimationFrame 包裝在一塊兒,確保僅在下一個動畫週期中再次調用它)。

你可能想知道爲何不隱含地告訴每一幀以前清除canvas。這是由於 WebGL 在繪製時會自動執行。若是它檢測到咱們更改了輸入變量,則默認狀況下會清除以前的緩衝區。若是出於某種緣由不是當前這種狀況咱們不但願清理畫布,那麼應該使用附加參數 const gl = canvas.getContext('webgl',{prepareDrawingBuffer: true});

繪製齒輪

對於每幀中的每一個齒輪,先把旋轉所需的矩陣 u_translationu_rotationu_moveOrigin 傳遞給GPU,而後開始繪製齒輪的每一個部分:

function drawGears() {
  gears.forEach((gear, index) => {
    const [centerX, centerY] = gear.center

    // u_translation
    gl.uniformMatrix3fv(
      translationLocation,
      false,
      translation(centerX, centerY)
    )

    // u_rotation
    gl.uniformMatrix3fv(rotationLocation, false, rotation(angles[index]))

    // u_moveOrigin
    gl.uniformMatrix3fv(
      moveOriginLocation,
      false,
      translation(-centerX, -centerY)
    )

    // 渲染齒輪
    renderGearPiece(gear)
    if (gear.children) gear.children.forEach(renderGearPiece)
  })
}

用相同的函數繪製齒輪的每一個部分:

function renderGearPiece({
  center,
  radius,
  fillColor,
  strokeColor,
  numberOfTeeth,
}) {
  const { TRIANGLE_STRIP, POINTS, TRIANGLES } = gl
  const coords = getCoords(gl, center, radius)

  if (fillColor) drawShape(coords, fillColor, TRIANGLE_STRIP)
  if (strokeColor) drawShape(coords, strokeColor, POINTS)
  if (numberOfTeeth) {
    drawShape(
      getCoords(gl, center, radius, numberOfTeeth),
      fillColor,
      TRIANGLES
    )
  }
}
  • 若是是帶邊界的圓 --> 使用 POINTS
  • 若是是彩色圓 --> 使用 TRIANGLE_STRIP
  • 若是是一個有齒的圓 --> 使用 TRIANGLES

經過使用各類 if,能夠建立一個填充有一種顏色但邊框是另外一種顏色的圓,或者建立一個填充有顏色和齒的圓。這意味着更大的靈活性。

實心圓和帶有邊界的圓的座標,即便一個是由三角形組成而另外一個是由點製成,也是徹底相同的。一個有着不一樣座標的帶齒的圓,也能夠用相同的代碼來獲取座標:

export default function getCoords(gl, center, radiusX, teeth = 0) {
  const toothSize = teeth ? 0.05 : 0
  const step = teeth ? 360 / (teeth * 3) : 1
  const [centerX, centerY] = center
  const positions = []
  const radiusY = (radiusX / gl.canvas.height) * gl.canvas.width

  for (let i = 0; i <= 360; i += step) {
    positions.push(
      centerX,
      centerY,
      centerX + (radiusX + toothSize) * Math.cos(2 * Math.PI * (i / 360)),
      centerY + (radiusY + toothSize) * Math.sin(2 * Math.PI * (i / 360))
    )
  }

  return positions
}

drawShape 的代碼與上一篇文章中看到的代碼相同:它將座標和顏色傳遞給 GPU,而後調用 drawArrays 函數來指示模式。

function drawShape(coords, color, drawingMode) {
  const data = new Float32Array(coords)
  const buffer = createAndBindBuffer(gl, gl.ARRAY_BUFFER, gl.STATIC_DRAW, data)

  gl.useProgram(program)
  linkGPUAndCPU(gl, { program, buffer, gpuVariable: 'position' })

  const inputColor = gl.getUniformLocation(program, 'inputColor')
  gl.uniform3fv(inputColor, color)
  gl.drawArrays(drawingMode, 0, coords.length / 2)
}

完成~

全部代碼

本文的全部代碼在 GitHub 上能夠找到,用 Preact 實現的。

總結

咱們學到如何用三角形和點生成更復雜的圖形,並實現了基於矩陣乘法的運動。

線(line)是一種咱們還沒有見過的繪圖模式。那是由於能夠用它製做的線很細,並不適合畫齒輪的齒。你不能輕易的更改線條的粗細,而要作到這一點,必須製做一個矩形(2個三角形)。這些線的靈活性很小,大多數圖形都是用三角形繪製的。不過你應該可以輕鬆使用給定 2 個 座標的 gl.LINES

本文是 WebGL 系列的第二部分。在本系列的下一篇文章中,咱們將學到紋理、圖像處理、幀緩衝區、3d對象等。

173382ede7319973.gif


本文首發微信公衆號:前端先鋒

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

歡迎繼續閱讀本專欄其它高贊文章:


相關文章
相關標籤/搜索