WebGL学习(二)变量、缓冲区、其他图形

220 阅读6分钟

1. 变量

我们在上一节的基础上增加一些变化:

  1. 绘制的点跟随鼠标点击位置移动
  2. 绘制的点双击过后改变颜色

效果

1.1 跟随点击移动

由于我们要更换位置,所以修改的就是顶点着色器代码

// 使用attribute声明变量
// 注意attribute只能用在顶点着色器里面

+ attribute vec4 pos;
void main() {
- gl_Position = vec4(0.5, 0.0, 0.0, 1.0);
+ gl_Position = pos;
  gl_PointSize = 30.0;
}

那么,变量的数据怎么来:

// 在主程序中
canvas.current.addEventListener('click', (event) => {
  const {
    target,
    offsetX,
    offsetY,
  } = event
  if(target instanceof HTMLCanvasElement) {
    const {
      width: eleW,
      height: eleH
    } = target.getBoundingClientRect()
    // 由于webgl的坐标和事件的坐标不一样,所以标准化一下
    // 计算原理就是算出相对值并归一化
    /**
    * html的坐标系原点在左上角,webgl的原点在中心所以苏姚转化
    * 1. 转化到canvas坐标系,(offsetX, offsetY)
    * 2. 转化到webgl坐标系,(offsetX - halfW, halfH - offsetY)
    * 3. 因为webgl是0~1,所以归一化((offsetX - halfW) / halfW, (halfH - offsetY) / halfH)
    */
    const halfW = eleW / 2
    const halfH = eleH / 2
    const clickX = (offsetX - halfW) / halfW
    const clickY = (halfH - offsetY) / halfH
    
    // 1. 获取着色程序program中的变量
    const pos = gl.getAttribLocation(program, 'pos')
    // 2. 设置变量值
    gl.vertexAttrib4fv(pos, [clickX, clickY, 0, 1])
    // 或者 gl.vertexAttrib4f(pos, clickX, clickY, 0, 1)
    
    // 注意重新渲染时需要clear一次
    gl.clearColor(0, 0, 0, 1)
    gl.clear(gl.COLOR_BUFFER_BIT)
    
    gl.drawArrays(gl.POINTS, 0, 1)
  }
})

1.2 双击换色

变化颜色就是修改片元着色器代码

// 使用uniform定义全局变量,使用变量前需要定义浮点数精度,因为片段着色器里面没有默认精度
// https://stackoverflow.com/questions/28540290/why-it-is-necessary-to-set-precision-for-the-fragment-shader
// 就是没讨论出来是否需要默认, OpenGL ES 2.0 有默认值
+ precision mediump float;
+ uniform vec4 color;
void main() {
- gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0)
+ gl_FragColor = color;
}

uniform实际上是全局变量,就是在一个渲染程序program内部的shader之间都是通用的,也就是说在顶点着色器里面定义uniform变量,在片段着色器里面也能用。

要注意的是全局变量属于单个着色程序,如果多个着色程序有同名全局变量,需要找到每个全局变量并设置自己的值。 我们调用gl.uniform???的时候只是设置了当前程序的全局变量,当前程序是传递给gl.useProgram 的最后一个程序。

主程序中:

canvas.current.addEventListener('dblclick', (e) => {
  // 1. 获取uniform变量
  const color = gl.getUniformLocation(program, 'color')
  // 2. 设置值
  gl.uniform4fv(color, [Math.random(), Math.random(), Math.random(), 1])
  
  gl.clearColor(0, 0, 0, 1)
  gl.clear(gl.COLOR_BUFFER_BIT)
  gl.drawArrays(gl.POINTS, 0, 1)
})

2.缓冲区

有时候我们需要批量生产很多个点,但是使用js循环重绘不是一个好的方法,特别是图形复杂之后,这个时候我们可以在缓存区里操作。

import { useEffect, useRef } from 'react'
import vert from './index.vert'
import frag from './index.frag'
const WebGL = () => {
  const canvas = useRef<HTMLCanvasElement>(null)
  useEffect(() => {
    if(canvas.current) {
      const gl = canvas.current.getContext('webgl')
      if(gl) {
        gl.clearColor(0, 0, 0, 1)
        gl.clear(gl.COLOR_BUFFER_BIT)
        const vertexShader = gl.createShader(gl.VERTEX_SHADER)
        const fragShader = gl.createShader(gl.FRAGMENT_SHADER)
        if(vertexShader && fragShader) {
          gl.shaderSource(vertexShader, vert)
          gl.shaderSource(fragShader, frag)
          
          gl.compileShader(vertexShader)
          gl.compileShader(fragShader)
          
          const program = gl.createProgram()
          if(program) {
            gl.attachShader(program, vertexShader)
            gl.attachShader(program, fragShader)
            
            gl.linkProgram(program)
            gl.useProgram(program)

            const pos = gl.getAttribLocation(program, 'pos')

            // 1. 创建缓冲对象
            const buffer = gl.createBuffer()

            /**
             * 2. 绑定缓冲对象到渲染程序
             * gl.ARRAY_BUFFER:表示缓冲区存储的是顶点的数据
             * gI.ELEMENT_ARRAY_BUFFER:表示缓冲区存储的是顶点的索引值(就是变量地址)
             */

            gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
            
            /**
             * 3. 给缓冲区赋值
             * 第一个参数和bindBuffer一样
             * 第三个参数:
             * gl.STATIC_DRAW:写入一次,多次绘制
             * gl.DYNAMIC_DRAW:写入多次,绘制多次
             */
            gl.bufferData(
              gl.ARRAY_BUFFER,
              new Float32Array([
                -0.5, 0.5,
                0.5, 0.5,
                0.5, -0.5,
                -0.5, -0.5,
              ]),
              gl.STATIC_DRAW
            )

            /**
             * 4. 告诉显卡读取数据并设定一些布局
             * 参数1 index:变量地址(顶点属性的索引)
             * 参数2 size:绘制所需要的数据维度
             * 参数3 type:数据格式
             *    gl.FLOAT:浮点型
             *    gl.UNSIGNED_BYTE:无符号字节
             *    gI.SHORT:短整型
             *    gl.UNSIGNED_SHORT:无符号整型
             *    gl.INT:整型
             *    gl.UNSIGNED_INT:无符号整型
             *    webgl2里面有gl.HALF_FLOAT
             * 参数4 normalized:数据是否归一化,gl.FLOAT无效
             * 参数5 stride:每一行之间的偏移量,两个顶点之间的
             * 参数6 offset:数据的偏移量
             */
            gl.vertexAttribPointer(pos, 2, gl.FLOAT, false, 0, 0)

            /**
             * 5. 激活属性变量
             */
            gl.enableVertexAttribArray(pos)

            gl.drawArrays(gl.POINTS, 0, 4)
          }
        }
      }
    }
  }, [])
  return (
    <div style = { {padding: 20, border: 'solid 2px #000', margin: 20, display: 'inline-block'} }>
      <canvas ref = { canvas } width = { 600 } height = { 600 }></canvas>
    </div>
  )
}
export default WebGL
// 顶点着色器
attribute vec4 pos;
void main() {
  gl_Position = pos;
  gl_PointSize = 30.0;
}
// 片段着色器
precision mediump float;
void main() {
  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}

效果

图片.png

3.多个缓冲区

简单的来说,就是创建多个缓冲区分别激活,现在我们把四个点分别设置大小为10、20、30、40

// .....
const size = gl.getAttribLocation(program, 'size')
const sizeBuffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, sizeBuffer)
gl.bufferData(
  gl.ARRAY_BUFFER,
  new Float32Array([
    10,
    20,
    30,
    40
  ]),
  gl.STATIC_DRAW)
// 注意这里的维度参数,不是二维了
gl.vertexAttribPointer(size, 1, gl.FLOAT, false, 0, 0)
gl.enableVertexAttribArray(size)

// 在drawArrays之前增加一个缓冲对象
gl.drawArrays(gl.POINTS, 0, 4)

顶点着色器

attribute vec4 pos;
+ attribute float size;
void main() {
  gl_Position = pos;
- gl_PointSize = 30.0;
+ gl_PointSize = size;
}

效果

图片.png

虽然满足了效果但是很傻,要是我要改变更多的变量不得重新写好多遍。还记得vertexAttribPointer这个函数的参数吗?最后两个参数表示的是行偏移量、列偏移量。那我们可不可以把上面代码中的位置、大小放入一个数组,然后设置不同偏移量装入缓冲区。

实现

import { useEffect, useRef } from 'react'
import vert from './index.vert'
import frag from './index.frag'
const WebGL = () => {
  const canvas = useRef<HTMLCanvasElement>(null)
  useEffect(() => {
    if(canvas.current) {
      const gl = canvas.current.getContext('webgl')
      if(gl) {

        gl.clearColor(0, 0, 0, 1)
        gl.clear(gl.COLOR_BUFFER_BIT)

        const vertexShader = gl.createShader(gl.VERTEX_SHADER)
        const fragShader = gl.createShader(gl.FRAGMENT_SHADER)

        if(vertexShader && fragShader) {
          gl.shaderSource(vertexShader, vert)
          gl.shaderSource(fragShader, frag)

          gl.compileShader(vertexShader)
          gl.compileShader(fragShader)

          const program = gl.createProgram()

          if(program) {
            gl.attachShader(program, vertexShader)
            gl.attachShader(program, fragShader)

            gl.linkProgram(program)
            gl.useProgram(program)

            const pos = gl.getAttribLocation(program, 'pos')
            const size = gl.getAttribLocation(program, 'size')

            // 1. 创建缓冲对象
            const buffer = gl.createBuffer()
            /**
             * 2. 绑定缓冲对象到渲染程序
             */
            gl.bindBuffer(gl.ARRAY_BUFFER, buffer)

            /**
             * 3. 给缓冲区赋值
             * 这里多了一个维度,添加了大小
             */
            const data = new Float32Array([
              -0.5, 0.5, 10,
              0.5, 0.5, 20,
              0.5, -0.5, 30,
              -0.5, -0.5, 40
            ])
            gl.bufferData(
              gl.ARRAY_BUFFER,
              data,
              gl.STATIC_DRAW
            )
            // 因为偏移量是按照字节为单位,所以获取每个元素的字节大小
            const dataSize = data.BYTES_PER_ELEMENT
            /**
             * 4. 告诉显卡读取数据并设定一些布局
             * 为什么stride设置为dataSize * 3?
             *  因为数据维度size取的2,如果stride设置为0,就会挨着两个两个的取
             */
            gl.vertexAttribPointer(pos, 2, gl.FLOAT, false, dataSize * 3, 0)
            /**
             * offset的值为dataSize * 2,因为size的值实际取的是index为2的值
             */
            gl.vertexAttribPointer(size, 1, gl.FLOAT, false, dataSize * 3, dataSize * 2)

            /**
             * 5. 激活属性变量
             */
            gl.enableVertexAttribArray(pos)
            gl.enableVertexAttribArray(size)

            gl.drawArrays(gl.POINTS, 0, 4)
          }
        }


      }
    }
  }, [])
  return (
    <div style = { {padding: 20, border: 'solid 2px #000', margin: 20, display: 'inline-block'} }>
      <canvas ref = { canvas } width = { 600 } height = { 600 }></canvas>
    </div>
  )
}

export default WebGL

4. 其他图形

4.1 线段

我们接着用上面的代码

// 只需要改这一步
- gl.drawArrays(gl.POINTS, 0, 4)
+ gl.drawArrays(gl.LINES, 0, 4)

drawArrays第一个参数就是绘制方式

效果

图片.png 可以发现是两两连接成了一条线,为什么是这样的呢?因为gl.LINES代表的就是绘制一系列单独线段。每两个点作为端点,线段之间不连接。

所以改成LINE_STRIP可以发现连起来了

- gl.drawArrays(gl.LINES, 0, 4)
+ gl.drawArrays(gl.LINE_STRIP, 0, 4)

效果

图片.png

其他的情况可以自己试试

4.2 三角形

同样的只需要改变参数

- gl.drawArrays(gl.LINES, 0, 4)
+ gl.drawArrays(gl.TRIANGLES, 0, 4)

这里需要注意,三角形只需要3个顶点,但是我这里给了4个,没关系,程序会截取3的倍数作为顶点

图片.png 我换个参数试试

- gl.drawArrays(gl.TRIANGLES, 0, 4)
// 三角扇
+ gl.drawArrays(gl.TRIANGLE_FAN, 0, 4)

使用三角扇的效果,字母是我加的 图片.png 为什么看着像正方形,因为三角扇的定义。这四个点画出来的三角形共享了同一个顶点也就是A,所以ABCACD两个三角形组成了现在这个样子。

再换个参数试试

- gl.drawArrays(gl.TRIANGLE_FAN, 0, 4)
// 三角带
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)

效果,标注是我额外加的: 图片.png 这里绘制了两个三角形123234,为什么选择这几个点。这里有个公式

4.2.1 TRIANGLE_STRIP理解

参考1

参考2

图片.png

4.2.2 TRIANGLE_STRIPTRIANGLE_FAN的区别

  1. 他们都是为了减少计算机运算量,而产生的的各种生成连续三角形的方法
  2. Triangle Strip中的三角形是相邻的三角形,而Triangle Fan的三角形指的是以一个中心点形成的平面上的连续三角形。
  3. 两者的主要区别在于Triangle Strip中的三角形是相邻的,而Triangle Fan中的三角形则不一定是连续的。