webGL入门(二)

上一次的作业

上一篇文章的最后留了个获取点击坐标渲染对应位置的像素点的作业,这边一开始来稍微说一下,其他的重复的就不多说了,主要来看画布的鼠标点击事件函数:

  const gl_Points = [];
  function handleMouseDown(e, gl, canvas, a_Position) {
    //鼠标点击坐标
    const clientX = e.clientX;
    const clientY = e.clientY;

    const rect = e.target.getBoundingClientRect();
    //进行坐标系转换
    const x = (clientX - rect.left - canvas.width / 2) / (canvas.width / 2);
    const y = (canvas.height / 2 - (clientY - rect.top) )/ (canvas.height / 2);
    //将坐标存入全局变量数组中保存
    gl_Points.push([x, y]);
    //在渲染保存的像素点的时候先使用清空函数
    gl.clear(gl.COLOR_BUFFER_BIT);
    //每次渲染的时候把所有的保存的像素点都重新渲染一遍
    for (let i = 0; i < gl_Points.length; i ++) {
      const position = gl_Points[i]
      gl.vertexAttrib3f(a_Position, position[0], position[1], 0.0);

      gl.drawArrays(gl.POINTS, 0, 1);
    }
  }

主要的东西注释已经写的很清楚,额外说一下getBoundingClientRect和坐标系转换。
getBoundingClientRect可以获取到指定元素在页面视口中的大小和位置,分别通过left、right、top、bottom、width、height来获取。然后是坐标,首先明确一点,坐标的计算是使用xy坐标轴的一侧来确定最后的比列,所以算出来分别离x轴或者y轴的距离之后要除以画布一半的宽或高来转换成(0,1)范围内的值,其他具体的看下面这个图应该就明白了:

webGL入门(二)

彩色的点

当我们已经完成了点击画布上的位置生成像素点的同时,不知道有没有想过既然坐标可以传递进去,做成动态生成的点,那我们可不可以每次生成像素点的时候传递进去每个点的颜色呢,让每次生成的点的颜色都不一样呢?

当然是可以的。首先明确一点,坐标是在顶点着色器中设置的,颜色是在片元着色器中设置的。然后就是在顶点着色器中我们使用了存储限定符attribute,这个是只能在顶点着色器中使用为顶点着色器修饰变量的,在片元着色器中我们要使用在两个着色器中都能使用的限定符uniform或者只能在片元着色器中使用把attribute间接传递到片元着色器的varying。这就意味着有两种方法都可以为像素点动态设置颜色。先说第一种uniform:

//片元着色器代码
const FSHADER_SOURCE = `
  precision mediump float;
  uniform vec4 u_FragColor;
  void main() {
    gl_FragColor = u_FragColor;
  }
`;

这里注意一下在着色器中定义float和int变量的时候要先用precision设定它的精度,那之前在顶点着色器中为什么不用设置精度呢,是因为顶点着色器有默认的修饰符highp,而片元着色器中是没有默认进度的,于是这里需要设置。和attribute修饰符类似,我们在要传递值的时候要先获取它的存储地址:

 const u_FragColor = gl.getUniformLocation(gl.program, "u_FragColor");

然后同样的使用类似的函数传递对应的值:

gl.uniform4f(u_FragColor, r, g, b, a);

和vertexAttrib3f类似,uniform4f也有一系列的函数,从1到4。这样只要在vertexAttrib3f的同时把颜色传递给片元着色器即可。

下面说第二种varying:

const VSHADER_SOURCE = `
  attribute vec4 a_Position;
  attribute vec4 a_Color;
  varying vec4 v_Color;

  void main(){
    v_Color = a_Color;
    gl_Position = a_Position;
  }
`;

const FSHADER_SOURCE = `
  precision mediump float;
  varying vec4 v_Color;

  void main(){
    gl_FragColor = v_Color;
  }
`;

varying的使用也是非常简单,只需要在顶点着色器和片元着色器中使用varying限定同样的变量,然后在顶点着色器中使用attribute来限定变量并传递即可。在传递数据的时候和之前传递坐标一样处理就行了。

从点到线前先等一下

在第一篇文章的最后解释作业的注意事项的时候提到过,我们没法同时保存住所有之前点击出来的点,每次绘制都需要绘制所有点。那问题来了,当我们想绘制一个线段或者其他多顶点图形的时候,我们需要同时传递多个点的坐标来绘制,这我们就需要自己引入新的缓冲区对象来帮忙保存数据。简单代码如下:

    const pointPosArr = new Float32Array([0.0, 0.5, -0.5, 0.0, 0.5, 0.0]);
    //创建缓冲区
    const buffer = gl.createBuffer()
    //链接缓冲区
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
    //将数据写入缓冲区
    gl.bufferData(gl.ARRAY_BUFFER, pointPosArr, gl.STATIC_DRAW);
    //将缓冲区对象分配给一个attribute变量
    gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0)
    //开启attribute变量
    gl.enableVertexAttribArray(a_Position);

现在按照顺序大致解释一下函数参数代码。
gl.createBuffer没啥好解释的,就是生成新的缓冲区对象,唯一需要提到的就是可以使用gl.deleteBuffer(buffer)删除指定的缓冲区。

gl.bindBuffer。它的第一个参数是target,代表了缓冲区的用途和存储的数据内容,有gl.ARRAY_BUFFER和gl.ELEMENT_ARRAY_BUFFER,前者包含了顶点的数据而后者包含了顶点的索引数据。第二个自然就是需要绑定的缓冲区。

gl.bufferData。将第二个参数里面的数据写入绑定到第一个参数上的缓冲区对象,我们并不能直接向缓冲区写入数据,只能向“target”写入数据,所以在写入之前要先绑定target。第三个参数代表程序将如何存储缓冲区对象中的数据,有gl.STATIC_DRAW(只会写入一次数据,但是需要绘制很多次)、gl.STREAM_DRAW(只写入一次数据,然后绘制若干次)、gl.DYNAMIC_DRAW(会写入多次并绘制很多次)。看起来有点难以区分,都是写入次数和绘制次数的区别,但实际上你暂时分不清这几个也没关系,bufferData的第三个参数是优化用的参数,如果传错了并不影响绘制,只会影响性能。在这里的数据必须得是类型化的数组数据。

这里插播一下类型化数组。这是一个可以使用new创建的指定数据类型的对象,并提供了一种用于在内存缓冲区中访问原始二进制数据的机制。就是平常所说的类数组或对象数组,虽然是个对象但是有索引可以使用下标访问数据,也可以使用set和get存取数据,还有个比较特殊的可以用到的BYTES_PER_ELEMENT表示每个元素的字节数。

gl.vertexAttribPointer,将缓冲区对象(实际上是缓冲区对象的地址或指针)分配给attribute变量。第一个参数是要分配的变量地址;第二个是size,即绘制单个图形需要的顶点数;第三个是和使用的类型化数组的类型对应,Float32Array(32位浮点数数组)对应的就是gl.FLOAT,其他的可以使用的时候再查询;第四个是normalize,表明是否将非浮点数的数据归一化到[0,1]或者[-1,1]区间,这里使用的是格式化好的浮点数数据,就不需要;第五个参数是stride(两个顶点之间的字节数),即要数组里面要使用的一组数据有几个字节,暂时就设置为0,现在或许有点难以理解什么意思,等后面遇到不为0的情况的时候再进行解释;最后一个就是offset偏移量,即之前的stride的一组字节数中被分配的变量从第几个字节开始,现在也是0,同样的后面不为0的情况下再进行解释。

gl.enableVertexAttribArray。用来开启指定的变量让顶点着色器可以获取对应变量的数据,并且可以使用disableVertexAttribArray来关闭链接。

另外提一下enableVertexAttribArray和vertexAttribPointer的名字从英文上看似乎和前面的缓冲区没啥关系,这个主要是webgl是从opengl继承过来的遗留问题,感兴趣的可以自行了解。

多个还是单个

我们这里将多个顶点数据存入缓冲区,来绘制多个顶点,同样的size和color数据都可以设置变量然后存入缓冲区中。很容易想到怎么弄,生成多个缓冲区然后存进去不就行了,就像这样:

    const vertexList = new Float32Array([
      0.5, 0.5, -0.5, -0.5, 0.5, -0.5
    ])

    const sizeList = new Float32Array([
      10.0, 5.0, 15.0
    ])

    const a_Position = gl.getAttribLocation(gl.program, "a_Position")
    const a_PointSize = gl.getAttribLocation(gl.program, "a_PointSize")

    const vertexBuffer = gl.createBuffer()
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, vertexList, gl.STATIC_DRAW)
    gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(a_Position)

    const sizeBuffer = gl.createBuffer()
    gl.bindBuffer(gl.ARRAY_BUFFER, sizeBuffer)
    gl.bufferData(gl.ARRAY_BUFFER, sizeList, gl.STATIC_DRAW)
    gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(a_PointSize)

似乎太繁琐了,重复的代码有点多。还记得之前提到的stride和offset参数吗,我们可以像下面这样修改:

     const vertexList = new Float32Array([
      0.5, 0.5, 10.0,
      -0.5, -0.5, 5.0,
      0.5, -0.5, 15.0
    ])

    const E_SIZE = vertexList.BYTES_PER_ELEMENT;


    const a_Position = gl.getAttribLocation(gl.program, "a_Position")
    const a_PointSize = gl.getAttribLocation(gl.program, "a_PointSize")

    const vertexBuffer = gl.createBuffer()
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, vertexList, gl.STATIC_DRAW)

    gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, E_SIZE * 3, 0);
    gl.enableVertexAttribArray(a_Position)

    gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, E_SIZE * 3, E_SIZE * 2);
    gl.enableVertexAttribArray(a_PointSize)

这里我们把每个顶点的position和size数据放在一起储存,然后只使用了一个缓冲区对象分别分配给两个attribute对象。主要来看两个vertexAttribPointer函数,它们的stride参数我们都设置成了E_SIZE * 3(E_SIZE是类型化数组的BYTES_PER_ELEMENT,之前介绍过),即position和size数据组成了一组数据需要三个字节,而从0开始的两个字节的数据是position,所以它的offset设置成了0,从2个字节之后的就是size数据,所以这里它的offset设置成了E_SIZE * 2。这里一定要注意,stride和offset都是字节数,而通过类型化数组可以很轻松的获取到数据单个的字节数是多少。

实际上,使用多个缓冲区对象来管理数据只适用于数据量不大的时候,当我们有相当大量级的顶点数据需要绘制的时候,只使用一个缓冲区对象包括了多种数据,然后通过机制管理并获取他们是更好的办法。

终于可以画线了

实际上到了这一步,画一个线段应该对于你没有任何难度了,我们只需要将drawArray中的gl.POINTS换成gl.LINES并且把第三个参数换成2(两点才能确定一条线),你会得到下面这个:

webGL入门(二)
在webgl中除了提供了LINES还提供了LINE_STRIP(线带)和LINE_LOOP(线环)两种,同样的,修改gl.LINES和第三个参数就行,这里要稍微注意一下,线段是只需要2个顶点就可以画出来的图形,即使你对第三个参数设置了2以上的数字,他也只会按2两个顶点的量去绘制,而后面两个则不一样,你设置了第三个参数为几他就会绘制出来几个点的图形。

LINE_STRIP:

webGL入门(二)

LINE_LOOP:

webGL入门(二)

作业

这次的作业有两个,不用担心都不是很难。

第一个是画一个五角星,不论是用线带或者线环都可以。

第二个是也弄一个画线段、线带和线环点击获取坐标绘制来生成的demo。

原文链接:https://juejin.cn/post/7212878112454377529 作者:saswhite

(1)
上一篇 2023年3月21日 下午7:10
下一篇 2023年3月21日 下午7:23

相关推荐

发表回复

登录后才能评论