优雅的学习webgl(2)—webgl中的着色器和缓冲区

在之前一章中我们了解了webgl中的一些常识,也尝试绘制了第一个webgl程序,接下来这一张会介绍一些webgl中的基本概念,包含顶点着色器、片元着色器、缓冲区

  • 什么是顶点着色器和片元着色器
  • 缓冲区

这个系列的源码地址为:源码的地址为: https://github.com/forthealll...

一、什么是顶点着色器和片元着色器

我们在第一篇文章中初略的介绍了着色器,在这里我们在详细讲讲什么是着色器。

1.顶点着色器

顶点着色器保存了所想绘制的图形的顶点的信息,包括了顶点的坐标,以及顶点的大小。我们结合GLSL语言,来详细的讲讲顶点着色器。

attribute vec4 a_position;
 
void main() {
   gl_Position = a_position;
   gl_PointSize = 10.0;
}

顶点着色器基本上具有两个系统的变量gl_Position和gl_PointSize,分别表示顶点的位置和顶点的大小。并且图形中的每一个顶点都会调用一次顶点着色器。顶点着色器可以使用attribute来修饰的全局变量。

上述例子中的:

attribute vec4 a_position;

a_position变量就是一个attribute修饰符修饰的全局变量,其变量的值可以在其他地方被赋值。我们可以在我们的js代码中:

var positionLoc = gl.getAttribLocation(someShaderProgram, "a_position");

通过gl实例来获取某个程序对象上的attribute修饰符修饰的变量a_position的地址,也可以给这个a_position变量赋新的值。

gl.vertexAttribPointer(positionLoc,newValue)

这里的全局变量听起来很拗口,其实全局的意思就是:

不仅仅只能在着色器中被使用,可以在着色器外被捕获或者谁用的变量,在GLSE语言中都是全局变量。

在顶点着色器中使用的全部变量除了用attribute修饰外,还可以通过uniform来修饰。比如我们需要在移动某个图形(给某个图形的每个顶点一个偏移)

//顶点着色器
attribute vec4 a_position;
uniform vec4 u_offset;
 
void main() {
   gl_Position = a_position + u_offset;
}
//顶点着色器以外的逻辑代码
var offsetLoc = gl.getUniformLocation(someProgram, "u_offset");
gl.uniform4fv(offsetLoc, [1, 0, 0, 0]);  // 向右偏移一半屏幕宽度

我们通过uniform这个全局变量,给顶点着色器赋值了一个偏移量。

在顶点着色器中attribute修饰的变量和uniform修饰的变量都可以作为全局变量在着色器外被取值和赋值。那么两者之前有什么区别?

attribute修饰的全局变量可以读取缓冲区中的值

至于什么是缓冲区,我们下节中会具体降到,简单来说缓冲区的存在使得可以在一次绘制中绘制出图形所有的顶点,而不需要手动的逐顶点绘制.

2.片元着色器

在前面一章中我们讲到片元着色器决定了图形的颜色,那么片元着色器是如何动作的呢。简单来说:

从顶点着色器中了如何装配图形,当顶点着色器画出了图形的轮廓后,片元着色器将图形分解成一个个小的片段,并且确定每一个片段的颜色。最后渲染出结果。

其实从顶点着色器到片元着色器,然后从片元着色器到渲染到浏览器的过程比较复杂,具体可以分为如下几步:

  • 初始化顶点着色器
  • 根据顶点着色器中的顶点值,开始图形装配
  • 图形装配完成后,开始光栅化
  • 光栅化后每一个片元都可以用内置变量gl.FragCoord来表示
  • 从片元(Frag)相对应的gl.FragCoord中读取信息并对每个片元生成独立的颜色信息gl.FragColor,并保存到颜色缓冲区
  • 当所有的片元都有有颜色信息后,将颜色缓冲区的图形渲染到浏览器中

用文字表述可能不够直观,我们来用图形表示:

我们以渲染一个正方形为例,在顶点着色器中我们只给出了4个顶点的坐标,光栅化后生成了9个小片元,光栅化后的小片元进入片元着色器中被处理,最后渲染到浏览器中。

我们在上一节中发现在片元着色器中我们仅仅传入了4个顶点的颜色,但是通过片元着色器,最后渲染出来的图形是一个彩色的正方形。

我们从顶点着色器中可以传递4个顶点的颜色信息给片元着色器,片元着色器接受这4个顶点的颜色信息,通过内插可以拿到每个片元的信息,最后渲染到浏览器中。

我们来了解一下内插的过程,首先我们可以通过varying修饰的变量将值从顶点着色器传递到片元着色器。

//顶点着色器

attribute vec4 a_color
varying vec4 v_color
void main() {
   v_color = a_color
}

//片元着色器

varying vec4 v_color
void main(){
   gl.FragColor = v_color
}

我们通过声明两个同名的用varying修饰的变量v_color,就可以实现将值从顶点着色器传递到片元着色器。

为什么我们需要将值从顶点传递到片元着色器呢?

因为片元着色器的颜色信息等可能与顶点的坐标有关,此外顶点着色器是唯一可以从缓存中读取值的地方.

我们接着来看插值的过程示意图:

内插发生在光栅化的时候,可以根据顶点的颜色值,内插得到所有片元的颜色值。简单距离比如两个顶点的颜色分别是(0,0,0)和(0,0,1),两个顶点间有10个片元,那么内插得到的几个片元的颜色分别为(0,0,0.1),(0,0,0.2)...(0,0,0.9)

总之:通过顶点着色器和片元着色器我们就能绘出各种图案,顶点着色器和片元着色器是webgl绘图的基础.

二、缓冲区

缓冲区的作用很简单,就是缓冲图形的信息,在一次绘制中将图形绘制出来。我们接下来一步步来看缓冲区,我们前面画了一个点:

const vsSource = `
    attribute vec4 a_Position;
    void main() {
      gl_Position = a_Position;
      gl_PointSize = 10.0;
    }
  `;

  // Fragment shader program

  const fsSource = `
    void main() {
      gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
    }
  `;
  const shaderProgram = initShaderProgram(gl, vsSource, fsSource)
  gl.clearColor(0.0, 0.0, 0.0, 1.0);  // Clear to black, fully opaque
  gl.clearDepth(1.0);                 // Clear everything
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
  gl.useProgram(shaderProgram);
  //开始绘制
  a_Position = gl.getAttribLocation(shaderProgram,'a_Position')
  gl.vertexAttrib3f(a_Position,0.0,0.0,0.0);
  gl.drawArrays(gl.POINTS,0,1);

上述代码的运行结果为:

如果我们要绘制3个点,那么需要修改上述的代码:

//开始绘制
  a_Position = gl.getAttribLocation(shaderProgram,'a_Position')
  gl.vertexAttrib3f(a_Position,0.0,0.0,0.0);
  gl.drawArrays(gl.POINTS,0,1);

  a_Position = gl.getAttribLocation(shaderProgram,'a_Position')
  gl.vertexAttrib3f(a_Position,0.0,0.1,0.0);
  gl.drawArrays(gl.POINTS,0,1);

  a_Position = gl.getAttribLocation(shaderProgram,'a_Position')
  gl.vertexAttrib3f(a_Position,0.0,-0.1,0.0);
  gl.drawArrays(gl.POINTS,0,1);

我们需要连续绘制3次才能出现3个点,绘制的结果如下图所示:

对于复杂的图形,我们不可能一个点一个点的去绘制,如果我们需要一次性的绘制多个点,那么就需要使用缓冲区,我们可以读取缓冲区内的顶点信息,然后一次性的绘制出来。

缓冲区是webgl系统中的一块存储区,可以在缓冲区对象保存绘制图形所需要的顶点信息。

使用缓冲区需要一下的五个步骤:

  1. 创建缓冲区(gl.createBuffer())
  2. 绑定缓冲区(gl.bindBuffer())
  3. 将数据写入缓冲区对象(gl.bufferData())
  4. 将缓冲区对象分配给一个attribute变量(gl.vertexAttribPointer())
  5. 开启attribute变量(gl.enableVertexAttribArray())

遵循上述的5个步骤,我们来使用缓冲区一次性绘制三个点,修改上面的代码:

const shaderProgram = initShaderProgram(gl, vsSource, fsSource)
  gl.clearColor(0.0, 0.0, 0.0, 1.0);  // Clear to black, fully opaque
  gl.clearDepth(1.0);                 // Clear everything
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
  gl.useProgram(shaderProgram);
  //开始绘制
  let vertices = new Float32Array([0.0,0.0,0.0,0.1,0.0,-0.1])  ---(1)
  //创建缓冲区
  let vertexBuffer = gl.createBuffer()                 ----(2)
  //将缓冲区对象绑定到目标
  gl.bindBuffer(gl.ARRAY_BUFFER,vertexBuffer)          ----(3)
  //向缓冲区对象写入数据
  gl.bufferData(gl.ARRAY_BUFFER,vertices,gl.STATIC_DRAW)      ---(4)
  let a_Position = gl.getAttribLocation(shaderProgram,'a_Position')
  //将缓冲区对象分配给a_Position
  gl.vertexAttribPointer(a_Position,2,gl.FLOAT,false,0,0)     ---(5)
  //开启attribute变
  gl.enableVertexAttribArray(a_Position);
  gl.drawArrays(gl.POINTS,0,3)

上述的代码同样的是绘制3个点,但是是在一次绘制中就完成了。具体的代码可以查看: https://github.com/forthealll...

来看几个需要注意的地方,首先在(1)中的Float32Array是一个类型化数组,类型化数组是JavaScript操作二进制数据的一个接口,并不需要引入其他任何插件。

webgl中需要浏览器和显卡进行通信,为了满足JavaScript与显卡之间大量的、实时的数据交换,它们之间的数据通信必须是二进制的,而不能是传统的文本格式。像C语言那样,直接操作字节,然后将4个字节的32位整数,以二进制形式原封不动地送入显卡,脚本的性能就会大幅提升。

let vertices = new Float32Array([0.0,0.0,0.0,0.1,0.0,-0.1])

此外就是将缓冲区对象分配给a_Position的(5)方法gl.vertexAttribPointer,该方法的接受6个参数,具体参数的意思这里不会去讲,值得注意的是这个方法将缓冲区的值传递给了顶点着色器中的attribute变量,并声明了如何在缓冲区中依次如何取值的方法。

gl.vertexAttribPointer(a_Position,2,gl.FLOAT,false,0,0)

这里这6个参数的意思是,缓冲区的attribute变量的引用a_Position,每次从缓冲区取2个点,缓冲区值的类型是FLOAT类型,false表示不会归一化,0表示从缓冲区数组的第0数组开始取,最后一个0表示间隔为0.

上述就是缓冲区的做用,上述的缓冲区只保存了顶点信息,实际上缓冲区可以保存顶点的坐标信息,颜色信息,光照信息,矩阵变换信息等等,同时缓冲区也可以有多个,顶点着色器可以使用attribute变量来读取缓冲区的值,同时片元着色器可以通过varying变量来接受从顶点着色器传递过来的值,从而在依次绘制中就可以得到完整的结果。

最后总结一下缓冲区和顶点着色器、片元着色器之间的通信:

我来评几句
登录后评论

已发表评论数()

相关站点

热门文章