天天看點

webgl——繪制一個旋轉的彩色立方體(四)前言一、整體代碼以及實作效果二、繪制步驟總結

文章目錄

  • 前言
  • 一、整體代碼以及實作效果
  • 二、繪制步驟
    • 1.建構頂點資料
    • 2.通過頂點索引建構立方體
    • 3. 執行動畫
    • 4. 其他注意細節
  • 總結

前言

前面文章介紹了如何通過多點來繪制圖形,通過建立緩沖區對象,将多個資料傳入到緩沖區中;然後 webgl 進行讀取緩沖區中的資料進行渲染。上個例子繪制 “F” 的坐标點不是很多;但是如果我們繪制一個立方體。如果還跟之前一樣的繪制方式;立方體的每一個面由兩個三角形組成,每個三角形有三個頂點,是以每個月需要有六個頂點,那麼頂點資料需要有 36 個,我們會發現其實我們隻需要用 8 個頂點來進行繪制立方體,但是如何進行組織繪制立方體的各個面呢?這時候我們需要一個索引。通過這個索引來記錄頂點然後,我們在構造頂點資料的時候傳入索引資料。webgl 通過索引去挨個讀取對應的頂點資料來繪制三維圖形。本篇文章也會講到如何進行立方體的渲染以及每個面不同顔色的着色等等。

一、整體代碼以及實作效果

首先還是先上代碼,如下:

import React, { useRef, useEffect } from 'react'
import { VSHADERR_SOURCE, FSHADER_SOURCE } from './glsl'
import { initShader } from '../../utils/webglFunc'
import { vertices, indices, colors } from './data'
import Matrix4 from '../../utils/matrix'
import './index.css'

const HelloCube = () => {
  const canvasDom = useRef<HTMLCanvasElement | null>(null)
  const requestID = useRef<number>()

  const initArrayBuffer = (gl: WebGLRenderingContext, data: Float32Array, num: number, type: number, attribute: string) => {
    var buffer = gl.createBuffer() // 建立緩沖區對象
    if (!buffer) {
      console.log('Failed to create the buffer object')
      return false
    }
    // 将資料寫入緩沖區對象
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
    gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW)
    var a_attribute = gl.getAttribLocation((gl as any).program, attribute)
    if (a_attribute < 0) {
      console.log('Failed to get the storage location of ' + attribute)
      return false
    }
    gl.vertexAttribPointer(a_attribute, num, type, false, 0, 0)
    // 将緩沖區對象配置設定給 attribute 變量
    gl.enableVertexAttribArray(a_attribute)
  
    return true
  }

  const initVertexBuffers = (gl: WebGLRenderingContext) => {
    // 建立緩沖區對象
    const indexBuffer = gl.createBuffer()
    if (!indexBuffer) {
      console.log('Failed to create the vertexColorBuffer object')
      return -1
    }
    
    // 将頂點坐标和顔色寫入緩沖區
    if (!initArrayBuffer(gl, vertices, 3, gl.FLOAT, 'a_Position')) {
      return -1
    }

    if (!initArrayBuffer(gl, colors, 3, gl.FLOAT, 'a_Color')) {
      return -1
    }

     // 将頂點索引寫入緩沖區對象
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer)
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW)

    return indices.length
  }

  const draw = (gl: WebGLRenderingContext, n: number, currentAngle: number, modelMatrix: any, u_ModelMatrix: any) => {
    console.log(currentAngle, 'currentAngle')
    // 設定旋轉矩陣
    modelMatrix.setRotate(currentAngle, 1, 0, 0)
    // 将旋轉矩陣傳遞給頂點着色器
    gl.uniformMatrix4fv(u_ModelMatrix, false, modelMatrix.elements)
    // 清空顔色和深度緩沖區
    gl.clear(gl.COLOR_BUFFER_BIT)
    // 繪制立方體
    gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0)
  }

  const main = () => {
    if (!canvasDom.current) return
    const gl = canvasDom.current.getContext('webgl')

    if (!gl) {
      console.log('Failed to get the rendering context for WebGL')
      return
    }
    gl.canvas.width = (gl as any).canvas.clientWidth
    gl.canvas.height = (gl as any).canvas.clientHeight
    
    // 初始化着色器
    if (!initShader(gl, VSHADERR_SOURCE, FSHADER_SOURCE)) {
      console.log('Failed to intialize shaders.')
      return
    }

    const n = initVertexBuffers(gl)
    if (n < 0) {
      console.log('Failed to set the vertex information')
      return
    }
    // 指定清空 canavs 的顔色
    gl.clearColor(0.0, 0.0, 0.0, 1.0)
    // 開啟隐藏面消除
    gl.enable(gl.DEPTH_TEST)

    // 建立 Matrix4 對象以進行模型變換
    const u_MvpMatrix = gl.getUniformLocation((gl as any).program, 'u_MvpMatrix')
    if (u_MvpMatrix && u_MvpMatrix < 0) {
      console.log('Failed to get the storage location of u_MvpMatrix')
      return
    }
    // 建立 Matrix4 對象以進行模型變換
    const mvpMatrix = new Matrix4()
    mvpMatrix.setPerspective(30, 1, 1, 100)
    mvpMatrix.lookAt(3, 3, 7, 0, 0, 0, 0, 1, 0)
    gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements)

    gl.viewport(0, 0, gl.canvas.width, gl.canvas.height)

    // 清空顔色和深度緩沖區
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)

    if (!n) return

    // 目前旋轉角度
    let currentAngle = 0.0  

    // 建立 Matrix4 對象以進行模型變換
    const u_ModelMatrix = gl.getUniformLocation((gl as any).program, 'u_ModelMatrix')
    if (u_ModelMatrix && u_ModelMatrix < 0) {
      console.log('Failed to get the storage location of u_ModelMatrix')
      return
    }
    // 建立 Matrix4 對象以進行模型變換
    const modelMatrix = new Matrix4()

    // 開始繪制三角形
    const trick = () => {
      let currentAngles = animate(currentAngle) // 更新角度
      if (currentAngle !== currentAngles) {
        currentAngle = currentAngles
        draw(gl, n, currentAngle, modelMatrix, u_ModelMatrix)
        requestID.current = requestAnimationFrame(trick) // 請求浏覽器調用 trick
      }
      // currentAngle += 0.01
      // draw(gl, n, currentAngle, modelMatrix, u_ModelMatrix)
      // requestID.current = requestAnimationFrame(trick) // 請求浏覽器調用 trick
    }
    trick()
  }
  // 記錄上一次調用函數的時刻
  let g_last = Date.now()
  // 記錄上一次調用函數的時刻
  const animate = (angle: number) => {
    // 計算距離上次調用經過多長時間
    const now = Date.now()
    const elapased = now - g_last // 毫秒
    console.log(g_last, 'g_last')
    g_last = now
    // 根據距離上次調用的時間,更新目前旋轉角度
    let newAngle = angle + (45.0 * elapased) / 1000.0
    return newAngle %= 360
  }

  useEffect(() => {
    main()
    return () => {
      requestID.current && window.cancelAnimationFrame(requestID.current)
    }
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [])

  return (
    <canvas ref={canvasDom}></canvas>
  )
}

export default HelloCube
           

着色器部分:

export const VSHADERR_SOURCE = `
  attribute vec4 a_Position;
  attribute vec4 a_Color;
  uniform mat4 u_ModelMatrix;
  uniform mat4 u_MvpMatrix;
  // varying 變量隻能是 float
  varying vec4 v_Color;
  void main() {
    // 如果 gl_Position 最後一個分量為 1.0,那麼前三個分量就可以表示一個點的三維坐标。平移不改變縮放比例,是以 u_Translation 第四個參數為 0.0,1.0   表示不縮放
    gl_Position = u_MvpMatrix * u_ModelMatrix * a_Position;
    v_Color = a_Color;
  }
`

export const FSHADER_SOURCE = `
  #ifdef GL_ES
  precision mediump float;
  #endif
  varying vec4 v_Color;
  void main() {
    gl_FragColor = v_Color;
  }
`
           

實作的效果:

webgl——繪制一個旋轉的彩色立方體(四)前言一、整體代碼以及實作效果二、繪制步驟總結

二、繪制步驟

1.建構頂點資料

如下圖:

webgl——繪制一個旋轉的彩色立方體(四)前言一、整體代碼以及實作效果二、繪制步驟總結

我們建構一個立方體如上圖,我們需要八個索引來表示立方體的八個點:

webgl——繪制一個旋轉的彩色立方體(四)前言一、整體代碼以及實作效果二、繪制步驟總結

頂點資料代碼如下(示例):

//    v6----- v5
//   /|      /|
//  v1------v0|
//  | |     | |
//  | |v7---|-|v4
//  |/      |/
//  v2------v3
// 類型化數組 -- 所有資料類型一緻,處理更高效
export const vertices = new Float32Array([
  // 頂點坐标和顔色
  1.0, 1.0, 1.0,  -1.0, 1.0, 1.0,  -1.0,-1.0, 1.0,   1.0,-1.0, 1.0,  // v0-v1-v2-v3 front
  1.0, 1.0, 1.0,   1.0,-1.0, 1.0,   1.0,-1.0,-1.0,   1.0, 1.0,-1.0,  // v0-v3-v4-v5 right
  1.0, 1.0, 1.0,   1.0, 1.0,-1.0,  -1.0, 1.0,-1.0,  -1.0, 1.0, 1.0,  // v0-v5-v6-v1 up
  -1.0, 1.0, 1.0,  -1.0, 1.0,-1.0,  -1.0,-1.0,-1.0,  -1.0,-1.0, 1.0,  // v1-v6-v7-v2 left
  -1.0,-1.0,-1.0,   1.0,-1.0,-1.0,   1.0,-1.0, 1.0,  -1.0,-1.0, 1.0,  // v7-v4-v3-v2 down
  1.0,-1.0,-1.0,  -1.0,-1.0,-1.0,  -1.0, 1.0,-1.0,   1.0, 1.0,-1.0   // v4-v7-v6-v5 back
])

export const colors = new Float32Array([
  0.4, 0.4, 1.0,  0.4, 0.4, 1.0,  0.4, 0.4, 1.0,  0.4, 0.4, 1.0,  // v0-v1-v2-v3 front(blue)
  0.4, 1.0, 0.4,  0.4, 1.0, 0.4,  0.4, 1.0, 0.4,  0.4, 1.0, 0.4,  // v0-v3-v4-v5 right(green)
  1.0, 0.4, 0.4,  1.0, 0.4, 0.4,  1.0, 0.4, 0.4,  1.0, 0.4, 0.4,  // v0-v5-v6-v1 up(red)
  1.0, 1.0, 0.4,  1.0, 1.0, 0.4,  1.0, 1.0, 0.4,  1.0, 1.0, 0.4,  // v1-v6-v7-v2 left
  1.0, 1.0, 1.0,  1.0, 1.0, 1.0,  1.0, 1.0, 1.0,  1.0, 1.0, 1.0,  // v7-v4-v3-v2 down
  0.4, 1.0, 1.0,  0.4, 1.0, 1.0,  0.4, 1.0, 1.0,  0.4, 1.0, 1.0   // v4-v7-v6-v5 back
])

// 頂點索引
export const indices = new Uint8Array([
  0, 1, 2,   0, 2, 3,    // front
  4, 5, 6,   4, 6, 7,    // right
  8, 9,10,   8,10,11,    // up
 12,13,14,  12,14,15,    // left
 16,17,18,  16,18,19,    // down
 20,21,22,  20,22,23     // back
])
           

indices 數組以索引的形式存儲了繪制頂點的順序。索引值是整型數,是以數組的類型是 Uint8Array(無符号 8 位整型數);indices 中每三個索引值為 1 組,指向三個點,由這個 3 個頂點組成 1 個三角形。通常我們不需要手動建立這些頂點和索引資料,因為三維模組化工具會幫助我們建立他們。

2.通過頂點索引建構立方體

我們之前繪制圖形時,一直使用 gl.drawArrays() 方法進行繪制;而通過索引繪制時,需要使用 gl.drawElements() 方法,我們需要在 gl.ELEMENT_ARRAY_BUFFER (不是之前的 gl.ARRAY_BUFFER) 中指定頂點的索引值,是以 gl.drawArrays() 和 gl.drawElements() 的差別就在于 gl.ELEMENT_ARRAY_BUFFER,它管理者具有索引結構的三維模型資料。gl.drawElements() 方法說明如下:

我們需要将頂點和顔色資料寫入到 target 為 gl.ARRAY_BUFFER 的緩沖區中,将索引資料寫入到 target 為 gl.ELEMENT_ARRAY_BUFFER 的緩沖區對象中。然後通過 gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0) 進行讀取資料進行繪制。

// 執行着色器,按照 mode 參數指定的方式,根據綁定到 gl.ELEMENT_ARRAY_BUFFER 的緩沖區中的頂點索引繪制圖形。
gl.drawElements(mode, count, type, offset)
           

參數:

1. mode:指定繪圖的方式,可以接收以下常量符号:
- gl.POINTS: 畫單獨的點。
- gl.LINE_STRIP: 畫一條直線到下一個頂點。
- gl.LINE_LOOP: 繪制一條直線到下一個頂點,并将最後一個頂點傳回到第一個頂點.
- gl.LINES: 在一對頂點之間畫一條線.
- gl.TRIANGLE_STRIP
- gl.TRIANGLE_FAN
- gl.TRIANGLES: 為一組三個頂點繪制一個三角形.

2. count:整數型 指定要渲染的元素數量。

3. type:枚舉類型 指定元素數組緩沖區中的值的類型。可能的值是:

- gl.UNSIGNED_BYTE
- gl.UNSIGNED_SHORT
- 當使用 OES_element_index_uint 擴充時: gl.UNSIGNED_INT

4. offset: 位元組機關 指定元素數組緩沖區中的偏移量。必須是給定類型大小的有效倍數.
           

我們需要将頂點索引(也就是三角形清單中的内容)寫入到緩沖區,并綁定到 gl.ELEMENT_ARRAY_BUFFER 上,調用 gl.drawElements() 時,webgl 首先從綁定到 gl.ELEMENT_ARRAY_BUFFER 的緩沖區中擷取頂點的索引值,然後根據索引值,從綁定到 gl.ARRAY_BUFFER 的緩沖區中擷取頂點的坐标、顔色等資訊,然後傳遞給 attribute 變量并執行頂點着色器。

在調用 gl.drawElements() 時,webgl 首先從綁定到 gl.ELEMENT_ARRAY_BUFFER 的緩沖區中擷取頂點的索引值,然後根據該索引值,從綁定到 gl.ARRAY_BUFFER 的緩沖區中擷取頂點的坐标、顔色等資訊,然後傳遞給 attribute 變量并執行頂點着色器。對每個索引值都這樣做,最後就繪制出了整個立方體。而此時你隻調用一次 gl.drawElement()。這種方式通過索引來通路頂點資料,進而循環利用頂點資訊,控制記憶體的開銷,但代價是你需要通過索引來間接地通路頂點,在某種程度上使程式複雜化了。是以,gl.drawElement() 和 gl.drawArrays() 各有優劣,具體用哪一個取決于具體的系統需求。

3. 執行動畫

為了讓一個立方體轉動起來,你需要做的是:不斷擦除和重繪立方體,并且在每次重繪時輕微地改變其角度。

首先我們需要在繪制之前進行設定請求背景色,設定好的背景色在重設之前一直有效。然後我們通過 requestAnimationFrame 實作反複調用繪制繪制方法。

對于 requestAnimationFrame API,大家可以在 mdn 檢視其用法,他是對浏覽器發出一次請求,請求在未來某個适當的時機調用 tick 函數方法。

代碼如下(示例):

// 開始繪制三角形
    const trick = () => {
      let currentAngles = animate(currentAngle) // 更新角度
      if (currentAngle !== currentAngles) {
        currentAngle = currentAngles
        draw(gl, n, currentAngle, modelMatrix, u_MvpMatrix)
        requestID.current = requestAnimationFrame(trick) // 請求浏覽器調用 trick
      }
    }
    trick()
  }
           

4. 其他注意細節

其他需要注意的就是我們在着色器中使用了之前沒有用到過的 varying 資料類型;之前提到過,他是頂點着色器跟片元着色器之間用來傳遞變量的資料類型。

事實上,我們把頂點的顔色指派給了頂點着色器中的 varying 變量 v_Color,他的值被傳給片元着色器中的同名、同類型變量。更準确地說,頂點着色器中的 varying 變量在傳入片元着色器之前經過了内插過程。是以,片元着色器中的 v_Color 變量和頂點着色器中的 v_Color 變量實際上并不是一回事,這也是為啥這種變量稱為 varying 變量的原因。我們隻是為每個頂點定義了顔色,但是頂點所在的面上的每個點都渲染成了那種顔色。其實這就是根據頂點的顔色進行内插得到的。後續我們會講到紋理相關,會繼續深入了解這塊。

還有一個就是用到的 Matrix4 類。他是用生成模型矩陣等變換矩陣的一個 js 庫,這裡先不展開說明,下一節會講述如何進行平移旋轉縮放等。

這裡還有一些相機朝向、以及投影矩陣相關的沒有講到;後續文章會一一介紹:

// 建立 Matrix4 對象以進行模型變換
    const modelMatrix = new Matrix4()
    modelMatrix.setPerspective(30, 1, 1, 100)
    modelMatrix.lookAt(3, 3, 7, 0, 0, 0, 0, 1, 0)
           

總結

至此,繪制一個旋轉的彩色立方體已完成;本節我們學到了如何将大量的點傳入到着色器中,以及如何簡化點的重複利用。通過索引可以簡化複雜的繪制。

繼續閱讀