WebGL入门(三):使用VUE3编写WebGL程序

5,680 阅读7分钟

前言

编程的学习,最重要的就是动手干!而开始动手前,首先得搭建好一个框架,所以今天给大家分享一下如何使用VUE3搭建一个WebGL项目。

这并不是说写WebGL项目非要什么ReactVUE这些前端库/框架才行,用原生JavaScript照样可以写的非常舒服和流畅。但是基于前端框架来写,还是有非常多的好处的:比如说公司是用VUE的,所以用VUE写一来可以契合公司的技术栈,二来还能学习一下VUE3。何乐而不为呢?

基于VUE3编写WebGL程序

首先我选择的是@vue/cli。因为对于vite来说,webpack更加熟悉一点。这样的话学习成本就不会很高,还能在控制的范围内。好,废话不多说,直接开干!

搭建VUE3

使用@vue/cli命令行搭建一个项目

vue create webgl-test

接着我会以JSX的方式来编写VUE代码,所以还要安装插件

npm install @vue/babel-plugin-jsx -D

接着配置babel,在babel.config.js中添加plugins属性:

{
  "plugins": ["@vue/babel-plugin-jsx"]
}

接下来,创建一个clickPoints.js文件,因为下面以是点击增加点来作为例子。

import { defineComponent, ref, onMounted } from 'vue';
export const ClickedPointes = defineComponent(({
    setup () {
        const root = ref(null);
        const canvasDown = (e) => {}
        return () => <canvas ref={root} onClick={canvasDown}  width="400" height="400"></canvas>
    }
}))

这样VUE的搭建大致就是这了(setup的语法可以看官方文档

编写WebGL

在编写之前,先说一下WebGL的流程。大致可分为这五个步骤:

  • 获取<canvas>元素,创建WebGL绘图上下文
  • 编写顶点着色器与片段着色器源代码
  • 创建着色器对象并载入以及编译着色器代码
  • 创建程序对象并插入和链接该着色器对象
  • 绘制

获取canvas元素,创建WebGL绘图上下文:
VUE中使用ref获取canvas元素

const root = ref(null);
onMount () {
    const canvas = root.value;
}
return () => <canvas ref={root}></canvas>

这样就能在onMount生命周期函数里面获取canvas元素。使用canvas来创建WebGL绘图上下文,为了方便将其抽象为一个函数。

const getWebglContext = (canvas) => {
  const ctx = canvas.getContext('webgl');
  return ctx
}
onMount () {
    const canvas = root.value;
    const gl = getWebglContext(canvas);
}

编写顶点着色器与片段着色器源代码:
着色器是一种使用类似于C的编程语言实现的精美视觉效果。编写着色器的语言也被称为着色器语言(shading language)OpenGL ES2.0给予OpenGL着色器语言(GLSL),因此后者也被称为OpenGL ES着色器语言(GLSL ES)WebGL是基于OpenGL ES2.0,所以也使用GLSL ES编写着色器。

JavaScript中,着色器程序是以字符串的形式“嵌入”其中的。WebGL需要两种着色器:

  • 顶点着色器(Vertex shader):顾名思义,顶点着色器是用于描述顶点的特性(位置、颜色等)。顶点指的是二维或三维空间的一个点。
  • 片段着色器(Fragment shader):在进行逐片段操作时的程序,片段(fragment)是一个WebGL的术语,可以简单的理解为像素。
const VShader = `
attribute vec4 a_Position;
void main() {
  gl_Position = a_Position;
  gl_PointSize = 10.0;
}
`
const FShader = `
void main() {
  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`

在这个例子中,着色器程序非常简单,只是设置了顶点位置和尺寸以及片段的颜色。注意gl_Position是一个内置变量且必须被赋值,否则着色器就无法正常工作。

JavaScript不同,GLSL ES是一种强类型语言,必须要明确的指定变量的类型。

  • float:表示浮点数
  • vec4:表示由四个浮点数组成的矢量 顶点着色器控制点的位置与尺寸,片段着色器控制点的颜色。它们都是由main函数开始执行。

创建着色器对象并载入以及编译着色器代码
为了能创建一个可以载入到GPU中且能够绘制几何图形的WebGL着色器。需要创建一个着色器对象,并把源代码载入到该对象中,然后编译、链接到这个着色器。因为顶点和片段两个着色器对象的创建是一样的,所以也可以将此步骤抽象为函数:

const loadShader = (gl, type, source) => {
  const shader = gl.createShader(type);
  if (shader === null) {
    console.log('unable to create shader')
    return null
  }
  gl.shaderSource(shader, source)
  gl.compileShader(shader)

  const compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS)
  if (!compiled) {
    gl.getShaderInfoLog(shader)
    console.log('Failed to compile shader')
    gl.deleteShader(shader)
    return null;
  }
  return shader;
}

1、使用gl.createShader方法创建一个着色器对象。参数可以取gl.VERTEX_SHADER或者gl.FRAGMENT_SHADER值。
2、然后使用gl.shaderSource方法将源代码载入到着色器对象中,第一个参数为已经创建好的着色器对象,第二个参数表示着色器的源代码。
3、载入后,调用gl.compileShader方法编译着色器。
4、最后使用gl.getShaderParameter方法检查编译状态。如出现编译错误,则使用gl.deleteShader方法删除该着色器对象。

创建程序对象并插入和链接该着色器对象
创建好着色器对象之后,还需要创建程序对象。

const createProgram = (gl, vshader, fshader) => {
  const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader)
  const flagShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader)
  if (!vertexShader || !flagShader) {
    return null
  }
  const program = gl.createProgram();
  if (!program) {
    return null;
  }
  gl.attachShader(program, vertexShader)
  gl.attachShader(program, flagShader)

  gl.linkProgram(program)
  var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
  if (!linked) {
    var error = gl.getProgramInfoLog(program);
    console.log('Failed to link program: ' + error);
    gl.deleteProgram(program);
    gl.deleteShader(flagShader);
    gl.deleteShader(vertexShader);
    return null;
  }
  gl.useProgram(program)
  gl.program = program
}
  • 调用gl.createProgram方法创建程序对象。
  • 并通过gl.attachShader方法把编译好的顶点着色器对象和片段着色器对象载入到该程序对象中。
  • 然后调用gl.linkProgram方法执行链接操作,如果链接成功就得到一个程序对象。
  • 调用gl.useProgram方法,告诉WebGL引擎可以用这个程序对象绘制图形 链接之后,WebGL实现把顶点着色器和片段着色器使用的使用的属性绑定到通用属性索引上。WebGL实现已为顶点的属性分配了固定数目的插槽,通用属性索引就是其中某个插槽的标识符。

绘制
经过一系列初始化之后在WebGL系统中建立了着色器。然后WebGL就会对着色器进行解析,辨识出着色器具有的attribute变量,每个变量都具有一个存储地址,以便通过存储地址向变量传输数据。比如向顶点着色器的a_Position变量传输数据,首先使用gl.getAttribLocation方法向WebGL系统请求该变量的存储地址。

let a_Position = gl.getAttribLocation(gl.program, 'a_Position');

该方法的第一个参数是程序对象,因为它包含了顶点着色器和片段着色器,第二个参数就是想要获取的变量名称。

得到该变量的存储地址后,就需要往该变量传输数据了,在这个例子中是通过点击来获取位置的。鼠标点击位置的信息存储在事件对象e中,可以通过e.clientXe.clientY来获取位置坐标。但这坐标不能直接用:

  • 鼠标点击的位置坐标是浏览器的坐标,并不是canvas元素的坐标
  • canvas的坐标系统与WebGL的坐标系统是不一样的,其原点位置和Y轴的正方向都不一样 image.png 首先将坐标从浏览器坐标系下转换到canvas坐标系下,然后再转换到WebGL坐标系下:
const getBounding = (e) => {
  const canvas = wRoot.value;
  let x = e.clientX;
  let y = e.clientY;
  const rect = e.target.getBoundingClientRect();
  x = ((x - rect.left) - canvas.width/2)/(canvas.width/2);
  y = (canvas.height/2 - (y - rect.top))/(canvas.height/2);
  return {
    x,
    y
  }
}
  • 使用getBoundingClientRect方法获取canvas坐标,rect.left与rect.top就是canvas原点。这样(x - rect.left)与(y - rect.top)就可以将浏览器坐标系下的坐标转换为canvas坐标系下的坐标。
  • canvas坐标系转换为WebGL坐标系,首先获取canvas的中心点(canvas.height / 2, canvas.width / 2)
  • 使用(x - rect.left) - canvas.width/2和canvas.height/2 - (y - rect.top)canvas原点平移到中心点
  • 最后canvas的x轴坐标区间为从0到canvas.width,而y轴区间从0到canvas.height。因为WebGL中轴的坐标区间从-1.0到1.0,所以将x,y坐标都除以中心点坐标就ok了。
const canvasDown = (e) => {
  const { x, y } = getBounding(e)
  g_points.push(x);
  g_points.push(y)
  gl.clear(gl.COLOR_BUFFER_BIT)
  const len = g_points.length;
  for (var i = 0; i < len; i+= 2) {
    gl.vertexAttrib3f(a_Position, g_points[i], g_points[i+1], 0,0)
    gl.drawArrays(gl.POINTS, 0, 1)
  }
}

使用gl.vertAttrib3f方法往a_Position变量传输数据。接着交给gl.drawArrays方法绘制点。
看看效果: clickpoints.gif

改造WebGL

虽然效果不错,但对于这个例子来说只是简单的点击屏幕渲染点而已,然而这个流程却太复杂了。还有一点就是这个流程对于每个WebGL程序来说都是一样的,所以是不是能够抽象出来呢?

而且前面说过着色器语言是编程语言,但在JavaScript里却只是字符串,完全没有给予相应的待遇!

基于这几点,我们来改造这个WebGL程序:
单独编写着色器语言
首先安装两个loader:

npm install glslify-loader raw-loader

接着配置webpack,在@vue/cli构建的项目中,要在vue.config.js文件中配置webpack

module.exports = {
  chainWebpack: config => {
    config.module
      .rule('webgl')
      .test(/\.(glsl|vs|fs|vert|frag)$/)
      .exclude
        .add(/node_modules/)
        .end()
      .use('raw-loader')
        .loader('raw-loader')
        .end()
      .use('glslify-loader')
        .loader('glslify-loader')
        .end()
  }
}

配置好后,如果不确定是否配置成功,可以使用以下命令将最终的webpack配置展示出来:

vue inspect > output.js

这样就能得到output.js文件,里面是webpack配置 image.png 然后安装两个vscode插件:

  • glsl-literal:用于语法高亮
  • GLSL Lint:用于代码检测 这样就能编写glsl文件了 image.png

使用REGL库整合WebGL流程
安装regl库:

npm install regl

使用require引入

import { defineComponent, ref, onMounted } from 'vue';
import VSHADER from './vshader.glsl';
import FSHADER from './fshader.glsl';
const regl = require('regl');
export const ClickedPointes = defineComponent(({
    setup () {
        const root = ref(null);
        ...
        const canvasDown = (e) => {
            const point = getBounding(e);
            gl.clear(gl.COLOR_BUFFER_BIT)
            g_points.push(point);
            g_points.forEach(item => {
                drawPoint(item);
            })
        };
        onMounted(() => {
          gl = getWebglContext(wRoot)
          const reglCtx = regl(gl);
          drawPoint = reglCtx({
            frag: () => FSHADER,
            vert: () => VSHADER,
            count: 1,
            primitive: 'points',
            attributes: {
              'a_Position': reglCtx.prop('point')
            }
          });
          gl.clearColor(0.0, 0.0, 0.0, 1.0);

          // Clear <canvas>
          gl.clear(gl.COLOR_BUFFER_BIT);
        })
        return () => <canvas ref={root} onClick={canvasDown}  width="400" height="400"></canvas>
    }
}))

现在只需要配置reglCtx,短短的几行代码就能代替之前的那几个流程。

一个基本的WebGL项目就到此为止,如果有更好的示例可以在评论区说一下,我学习一下!

结尾

更多文章请移步楼主github,如果喜欢请点一下star,对作者也是一种鼓励。