【《WebGL编程指南》读书笔记-WebGL入门】

《WebGL编程指南》读书笔记
目录链接:https://blog.csdn.net/floating_heart/article/details/124001572

第二章 WebGL入门

在H5之前,网页通过<img>标签显示静态的图片,不能实时绘制和渲染。后来,出现了一些第三方解决方案,如Flash Player等。H5引入了<canvas>标签,允许JavaScript动态地绘制图形。

WebGL程序在屏幕上同时使用HTML和JavaScript来创建和显示三维图形,采用H5中新引入的<canvas>元素(标签),它定义了网页上的绘图区域。如果没有WebGL,JavaScript只能在<canvas>上绘制二维图形,有了WebGL,就可以在上面绘制三维图形了。

初识绘图流程、缓冲区、着色器、attribute和uniform变量

DrawingRectangle示例与绘图基本三步骤

我们首先从一个简单的示例程序DrawingRectangle了解一下由<canvas>提供的核心程序,该函数在页面上绘制了一个实心的蓝色矩形。下图为示例程序在浏览器上的效果:

让我们看一下示例程序中HTML是如何使用<canvas>标签,以及DrawRectangle函数是如何工作的:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body onload="main()">
    <canvas id="example" width="400" height="400">
      Please use a browser that supports "canvas"
    </canvas>
    <script src="./js/DrawRectangle.js"></script>
  </body>
</html>
// DrawRectangle.js
function main() {
  // 获取<canvas>元素
  let canvas = document.getElementById('example')
  if (!canvas) {
    console.log('Failed to retrieve the <canvas> element')
    return
  }

  // 获取绘制二维图形的绘图上下文
  let ctx = canvas.getContext('2d')

  // 绘制蓝色矩形
  ctx.fillStyle = 'rgba(0,0,255,1.0)'
  ctx.fillRect(120, 10, 150, 150)
}

从h5的代码中可以看到,<canvas>标签的使用与其他标签类似,也可以赋予id和一些属性值,这些基础的前端知识不再细说。

从JavaScript函数中可知,在<canvas>上绘制二维图形(2D图形),需经过以下三个步骤

  1. 获取<canvas>元素;
  2. 向该元素请求二维图形的“绘图上下文”(context);
  3. 在绘图上下文上调用相应的绘图函数,以绘制二维图形。

不论绘制二维还是三维的图形,这三个步骤是一样的,绘制三维图形时只需要在第二步请求三维图形的context即可。

在此我们对示例中的代码进行简单解析:

  • 获取<canvas>元素:

示例中采用document挂载的getElementById()方法,通过html标签的id获取元素。属于简单的前端内容。

  • 获取图形上下文

采用canvas元素挂载的方法getContext()方法获取上下文,如果上下文没有定义则返回null。

方法的参数制定了上下文的类型,详情可见MDN中HTMLCanvasElement.getContext()

此处获取的上下文类型是CanvasRenderingContext2D

  • 使用上下文支持的方法绘制图形

示例中使用了两个方法,注释如下:

  ctx.fillStyle = 'rgba(0,0,255,1.0)'  // 设置填充颜色为蓝色
  ctx.fillRect(120, 10, 150, 150)  // 用设置的颜色填充矩形

第1行将字符串’rgba(0,0,255,1.0)'赋值给ctx对象(CanvasRenderingContext2D对象/canvas 2d的上下文)的fillStyle属性。

第2行调用ctx对象的fillRect()方法,方法的语法为context.fillRect(x,y,width,height),x和y为矩形左上角的坐标,width和height为矩形的宽度和高度,以像素计。<canvas>中的坐标系统和OpenGL一致,二维的坐标系统如下图所示:

这一部分的api与OpenGL较为相似。

清空绘图区示例与背景色设置(初识多缓冲区模型与颜色缓冲区)

上一个示例还不能严格认为是WebGL的程序,而是<canvas>标签原本的2d方法和CanvasRenderingContext2D对象,下面进入WebGL的简单示例。

HelloCanvas示例被认为时最短的WebGL程序,其功能是使用背景色清空<canvas>标签的绘图区,效果如下:

在这里插入图片描述

因笔者未找到书中自带的函数库,所以对代码进行了修改,修改后代码如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body onload="main()">
    <canvas id="webgl" width="400" height="400">
      Please use a browser that supports "canvas"
    </canvas>
    <!-- 书中的函数库 -->
    <!-- <script src="../lib/wegbl-utils.js"></script> -->
    <!-- <script src="../lib/wegbl-debug.js"></script> -->
    <!-- <script src="../lib/cuon-utils.js"></script> -->
    <script src="./js/HelloCanvas.js"></script>
  </body>
</html>
// HelloCanvas.js
function main() {
  // 获取<canvas>元素
  let canvas = document.getElementById('webgl')

  // 获取WebGL绘图上下文
  // let gl = getWebGLContext(canvas) (书中的函数)
  let gl = canvas.getContext('webgl')
  if (!gl) {
    console.log('Failed to get the rendering context for WebGL')
    return
  }

  // 指定清空<canvas>的颜色
  gl.clearColor(0.0, 0.0, 0.0, 1.0) // 黑色

  // 清空<canvas>
  gl.clear(gl.COLOR_BUFFER_BIT)
}

解析:

h5的代码中创建了一个canvas标签,与上一个示例大同小异,注释中的部分是书中代码库的引用。

JavaScript中的函数同样分为三个部分:获取canvas元素、获取绘图上下文和对上下文进行操作,第一步与上一个示例相同,后两步解析如下:

获取绘图上下文:

代码中注释的语句是书中引用的函数(getWebContext()),是本书编写的获取上下文函数,可以隐藏不同浏览器之间的差异,需引用书中的代码库来使用。

在此处,笔者采用上一个示例的方法canvas.getContext(),输入webgl参数,获取WebGLRenderingContext对象赋值给gl,以匹配后面的方法,更多的参数可以参考MDN。

关于不同浏览器之间的差异,后文可能会涉及,此不并不清楚,先把书中的解释写在下面:

虽然大部分浏览器都接收字符串"expeimental-webgl",但并非所有浏览器都这样。而且,过一段时间,参数将变成webgl,所以我们选择将这些细节隐藏起来。

绘图上下文支持的操作:
此处采用了两个webgl相关方法gl.clearColor()gl.clear()

  • 设置<canvas>背景色

gl.clearColor(red,green,blue,alpha):指定绘图区域的背景色。
参数:
red: 指定红色值(0.0~1.0);
green: 指定绿色值(0.0~1.0);
blue: 指定蓝色值(0.0~1.0);
如果任何值小于0.0或者大于1.0,那么就会分别截断为0.0或1.0
**返回值:**无
**错误:**无

关于这一方法,可能有三点需要说明:

  1. 在第一个示例中,颜色分量值在0~255之间,但由于WebGL继承自OpenGL,所以它遵循传统的OpenGL颜色分量取值范围,即从0.0到1.0。RGB值越高,颜色就越亮;类似,第四分量α的值越高,颜色就越不透明。
  2. 一旦指定背景色后,背景色会贮存在WebGL系统中,在下次调用gl.clearColor()方法之前不会改变。所以,如果未来还需要再次清空绘图区,没必要再指定一次颜色。
  3. 这是挂载在WebGLRenderingContext对象上的方法。
  • 清空绘图区

示例中所说的清空绘图区,在WebGL中可以理解为:清空颜色缓冲区(color buffer)。

gl.clear(buffer):将指定的缓冲区设定为预定的值。如果清空的是颜色缓冲区,那么将使用gl.clearColor()指定的值(作为预定值)。
参数:
buffer:指定待清空的缓冲区,位操作符OR(|)可用来指定多个缓冲区
gl.COLOR_BUFFER_BIT:指定gl对象的颜色缓冲区
gl.DEPTH_BUFFER_BIT:指定gl对象的深度缓冲区
gl.STENCIL_BUFFER_BIT:指定gl对象的模板缓冲区
**返回值:**无
**错误:**INVALID_VALUE:缓冲区不是以上三种类型

缓冲区的默认参数和相关函数如下:
在这里插入图片描述

WebGL中的gl.clear()方法继承自OpenGL,基于多基本缓冲区模型。关于颜色缓冲区和深度缓冲区会在书中进行介绍,模板缓冲区使用较少故没有介绍,笔者可能会在之后进行补充。

绘制一个点(版本1)与着色器的初识(静态着色器)

相关内容:着色器初识、使用着色器的WebGL程序结构、初始化着色器、顶点着色器及内置变量、片元着色器及内置变量、齐次坐标、WebGL坐标系统、JavaScript程序和着色器程序的协同运行机制
相关函数:initShaders(), vec4(), gl.drawArrays()

示例程序将在原点(0.0, 0.0, 0.0)处的10个像素大的红色的点。
因为WebGL处理的是三维图形,所以我们有必要为这个点指定三维坐标。
示例的效果如下图所示:
在这里插入图片描述

代码如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Draw a point (1)</title>
  </head>
  <body onload="main()">
    <canvas id="webgl" width="400" height="400">
      Please use the browser supporting "canvas"
    </canvas>
    <!-- 书中的函数库 -->
    <script src="./libs/webgl-utils.js"></script>
    <script src="./libs/webgl-debug.js"></script>
    <script src="./libs/cuon-utils.js"></script>
    <script src="./js/HelloPoint1.js"></script>
  </body>
</html>
// HelloPoint1.js
// 顶点着色器程序
var VSHADER_SOURCE =
  'void main() {\n' +
  ' gl_Position = vec4(0.0, 0.0, 0.0, 1.0);\n' + // 设置坐标
  ' gl_PointSize = 10.0;\n' + // 设置尺寸
  '}\n'
// 片元着色器程序
var FSHADER_SOURCE =
  'void main() {\n' +
  ' gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n' + // 设置颜色
  '}\n'
// 主程序
function main() {
  // 获取<canvas>元素
  let canvas = document.getElementById('webgl')
  // 获取WebGL绘图上下文
  // let gl = getWebGLContext(canvas)
  let gl = canvas.getContext('webgl')
  if (!gl) {
    console.log('Failed to get the rendering context for WebGL')
    return
  }
  // 初始化着色器
  if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
    console.log('Failed to initialize shaders')
    return
  }
  // 设置<canvas>背景色
  gl.clearColor(0.0, 0.0, 0.0, 1.0)
  // 清空<canvas>
  gl.clear(gl.COLOR_BUFFER_BIT)
  // 绘制一个点
  gl.drawArrays(gl.POINTS, 0, 1)
}

汗!!!!!
这次示例中初始化着色器函数initShaders囊括了很长的流程,自己写的话对于初学者来说太复杂了,所以笔者还是找到了书中的代码库引入了此方法。
这部分代码库后续会考虑以何种方式分享出来。

DrawingRectangle示例中通过绘图上下文CanvasRenderingContext2D非常简单地绘制了一个矩形,但这在WebGL中,事情不再简单:WebGL依赖于一种新的称为**着色器(shader)**的绘图机制,着色器强大,但也更为复杂。这是WebGL一项重要的核心机制,要使用WebGL绘图就必须使用着色器。而在代码中,着色器程序是以字符串的形式“嵌入”在JavaScript文件中的,如示例所示,在程序真正开始运行前它就已经设置好了。

WebGL中有两种着色器:

  • 顶点着色器(Vertex shader):顶点着色器是用来描述顶点特性(如位置、颜色等)的程序。顶点(vertex)是指二维或三维空间中的一个点,比如二维或三维图形的端点或交点。
  • 片元着色器(Fragment shader):进行逐片元处理过程如光照的程序。片元(fragment)是一个WebGL术语,你可以将其理解为像素(图像的单元)。

与一般的JavaScript程序一样,示例整体的运行过程从浏览器执行JavaScript程序开始,最终将程序运行的结果显示到浏览器中。加载着色器的WebGL程序运行流程如下:

在这里插入图片描述

首先运行JavaScript程序,调用了WebGL的相关方法,然后顶点着色器和片元着色其就会执行,在颜色缓冲区内进行绘制,此时就清空了绘图区;最后,颜色缓冲区中的内容会自动在浏览器的<canvas>上显示出来。

在示例程序中:

  • 顶点着色器规定了点的位置为(0.0, 0.0, 0.0),尺寸是10.0像素。
  • 片元着色器规定了点的颜色为红色(1.0, 0.0, 0.0, 1.0)。

加载着色器的WebGL程序的结构:

示例HelloPoint1.js的代码可以分为三部分:顶点着色器程序、片元着色器程序和主程序。

  • 我们首先看一下新出现的两段着色器程序。
// 顶点着色器程序
var VSHADER_SOURCE =
  'void main() {\n' +
  ' gl_Position = vec4(0.0, 0.0, 0.0, 1.0);\n' + // 设置坐标
  ' gl_PointSize = 10.0;\n' + // 设置尺寸
  '}\n'
// 片元着色器程序
var FSHADER_SOURCE =
  'void main() {\n' +
  ' gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n' + // 设置颜色
  '}\n'

与主程序不同,着色器使用类似于C的OpenGL ES着色器语言(GLSL ES)来编写,同时需要预先处理称单个字符串的形式(故用+连接)。此处使每个字符串以’\n’结束,是因为着色器内部出错是,能获得出错的行号,对于检查源代码中的错误很有帮助。('\n’不是必须的,也可以不使用。)着色器出错时,会出现如下类似提示:

Failed to compile shader: ERROR: 0:4: ')' : syntax error

着色器代码作为字符串存储在变量VSHADER_SOURCE和FSHADER_SOURCE中,供后面主程序调用。

关于如何从文件中加载着色器程序,可以参见本书附录F“从文件中加载着色器程序”

  • 主程序
// 主程序
function main() {
  // 获取<canvas>元素
  let canvas = document.getElementById('webgl')
  // 获取WebGL绘图上下文
  // let gl = getWebGLContext(canvas)
  let gl = canvas.getContext('webgl')
  if (!gl) {
    console.log('Failed to get the rendering context for WebGL')
    return
  }
  // 初始化着色器
  if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
    console.log('Failed to initialize shaders')
    return
  }
  // 设置<canvas>背景色
  gl.clearColor(0.0, 0.0, 0.0, 1.0)
  // 清空<canvas>
  gl.clear(gl.COLOR_BUFFER_BIT)
  // 绘制一个点
  gl.drawArrays(gl.POINTS, 0, 1)
}

main()函数的执行流程如下图所示,与上一个示例相比,此处增加了初始化着色器和绘图两步,大部分的WebGL程序都遵循这样的流程,我们后面将对增加的两部分进行说明。

在这里插入图片描述


初始化着色器

  // 初始化着色器
  if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
    console.log('Failed to initialize shaders')
    return
  }

示例中调用了辅助函数initShaders()对字符串形式的着色器进行初始化,该函数被定义在cuon.util.js中,该函数介绍如下:

gl.initShaders(gl, vshader, fshader):在WebGL系统内部建立和初始化着色器。
参数:
gl: 指定渲染上下文;
vshader: 指定顶点着色器程序代码(字符串);
fshader: 指定片元着色器程序代码(字符串);
返回值:
true:初始化着色器成功
false:初始化着色器失败

关于这一函数的细节会在书的第九章进行说明。此处我们需要了解该函数的行为:
在这里插入图片描述

此处的WebGL系统经过了简化,会在书中第十章仔细讨论其细节

从上半图可以了解到,WebGL系统由两部分组成,即顶点着色器和片元着色器。在初始化之前,两个着色器都是空白的,initShaders()需要将字符串形式的着色器代码冲JavaScript传给WebGL系统,并建立着色器。注意:着色器运行在WebGL系统中,而不是JavaScript程序中。(也就有了浏览器是否支持WebGL一说)

下半图展示了initShaders()执行后的情形,WebGL系统中已经建立好了着色器后。顶点着色器先执行,它对gl_Posetion变量和gl_PointSize变量进行赋值,并将他们传入片元着色器,然后片元着色器再执行。

实际上,片元着色器并不直接接收两个变量,而是接收经过光栅化处理后的片元值,这一部分会在书的第五章进行讨论

所以,WebGL程序包括运行在浏览器中的JavaScript和运行在WebGL系统中的着色器程序两个部分

下面我们将对两种着色器的定义进行解释。


顶点着色器:

// 顶点着色器程序
var VSHADER_SOURCE =
  'void main() {\n' +
  ' gl_Position = vec4(0.0, 0.0, 0.0, 1.0);\n' + // 设置坐标
  ' gl_PointSize = 10.0;\n' + // 设置尺寸
  '}\n'

顶点着色器设置了点的位置和大小。该语言本身和C语言一样,必须包含一个main()函数,main()函数前面的关键字void表示这个函数没有返回值,同时也不能指定参数。

与大多数语言一样,着色器程序使用=操作符进行赋值,将点的位置和尺寸分别赋值给gl_Positiongl_PointSize,这两个变量是内置在顶点着色器之中的:

类型变量名描述
vec4gl_Position表示顶点的位置
floatgl_PointSize表示点的尺寸(像素数)

注:gl_Position必须被赋值,否则着色器无法正常工作;gl_PointSize并不是必须的,默认值为1.0。

关于这两者的数据类型,如下图所示:
在这里插入图片描述

如果类型赋值错误,如10.0变成10,就会报错:Failed to compile shader: ERROR: 0:3: 'assign' : cannot convert from 'const int' to 'PointSize mediump float'
gl_Position表示顶点的位置,一般顶点的位置只有三位坐标,即(X, Y, Z)分别代表X、Y和Z坐标值,此处采用着色器内置函数vec4()创建vec4类型的变量:

vec4(v0, v1, v2, v3):根据v0,v1,v2,v3值创建vec4对象
参数:
v0, v1, v2, v3: 指定4个浮点型分量;
返回值:
由v0, v1, v2, v3组成的vec4对象。

在示例中,我们添加了1.0作为函数第四分量。在WebGL(或者说OpenGL)中,将gl_Positon设置为vec4类型的变量实际上是采用了齐次坐标的方式,因为它能提高处理三维数据的效率,所以在三维图形系统中被大量使用。关于齐次坐标可以看下面的说明:
在这里插入图片描述


片元着色器:

// 片元着色器程序
var FSHADER_SOURCE =
  'void main() {\n' +
  ' gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n' + // 设置颜色
  '}\n'

片元着色器控制点的颜色,片元就是显示在屏幕上的一个像素,包括对应像素的位置、颜色和其他信息,片元着色器就是处理片元,使其显示在屏幕上。

与顶点着色器类似,片元着色器将颜色信息赋值给变量gl_FragColor,该变量的说明如下:

类型变量名描述
vec4gl_FragColor指定片元颜色(RGBA格式)

变量类型为vec4,包括四个浮点型分量,分别代表RGBA值。示例中(1.0, 0.0, 0.0, 1.0)代表红色。


绘制操作:

  // 绘制一个点
  gl.drawArrays(gl.POINTS, 0, 1)

建立着色器之后,与HelloCanvas.js示例一样,进行设置背景色和清空绘制区域的操作,最后进行绘制操作。绘制操作采用的方法为gl.drawArrays()

gl.drawArrays(mode, first, count):执行顶点着色器,按照mode参数指定的方式绘制图形。
参数:
mode: 指定绘制的方式,可接收以下常量符号:gl.POINTS, gl.LINES, gl.LINE_STRIP, gl.LINE_LOOP, gl.TRIANGLES, gl.TRIANGLE_STRIP, gl.TRIANGLE_FAN;
first: 指定从哪个顶点开始绘制(整型数);
count: 指定绘制需要用到多少个顶点(整型数);
**返回值:**无
错误:
INVALID_ENUM:传入的mode参数不是前述参数之一;
INVALID_VALUE:参数first或count是负数

示例程序函数参数设置如下:

  • 因为我们绘制的是单独的点,所以设置第一个参数为gl.POINTS;
  • 设置第二个参数为0,表示从第1个顶点(虽然只有一个顶点)开始画起;
  • 第三个参数count为1,表示程序中只绘制1个点

示例程序函数运行过程如下:

  • 调用gl.drawArrays()时,顶点着色器执行count次,每次处理1个顶点。在这个示例程序中,着色器执行1次,只绘制一个点。在顶点着色器执行的时候,将调用并逐行执行下图WebGL系统顶点着色器中的main()函数,为gl_Position和gl_PointSize赋值。

在这里插入图片描述

  • 顶点着色器执行完毕后,片元着色器就会开始执行,同样调用main()函数,为gl_FragColor赋值。

至此,我们应当对顶点着色器和片元着色器的工作方式由了大致的了解,图中流程是简化了很多倍的WebGL系统运行过程,完整的过程一般称作WebGL渲染管线。完整的渲染管线在可能会在书的后面提到,如果没有提到,再进行补充。


WebGL坐标系统:

由于WebGL处理的是三维图形,所以它使用三维坐标系统(笛卡尔坐标系),具有X轴、Y轴和Z轴。在WebGL中,当面向计算机屏幕时,X轴时水平的(正方向为右),Y轴时垂直的(正方向为上),Z轴垂直于屏幕(正方向向外),可以认为是我们常说的右手坐标系(right-handed coordinate system)。
在这里插入图片描述

  1. 书中坐标系的图片没错,但文字似乎说反了
  2. 事实上,WebGL本身既不是右手坐标系,也不是左手坐标系,这一点在附录D“WebGL/OpenGL:左手坐标系还是右手坐标系?”有详细的讨论。

由此可见,WebGL坐标系和<canvas>绘图区的坐标系不同,需要将前者映射到后者。在<canvas>绘图区,WebGL坐标系统如下图所示:
在这里插入图片描述

了解这些后,可以通过示例程序进行试验,增进对坐标系统的认识


绘制一个点(版本2)与attribute变量(动态设置顶点着色器)

相关内容:用JavaScript程序从着色器外部向着色器传输数据(动态,而不是静态写在着色器中)、attribute变量、uniform变量
相关函数:gl.getAttribLocation()、gl.vertexAttrib3f()及其同族函数

小结:动态设置着色器参数,主要包括以下几步:1.在着色器语言中,设置attribute或uniform变量,将变量赋值到主函数参数中;2.JavaScript程序中通过相关函数获取变量存储地址;3.通过相关函数,向该地址传输数据。

前一节我们了解到如何绘制一个点,学习了绘制点用到的着色器核心函数,理解了WebGL程序的基本行为。这一节将讨论如何在JavaScript和着色器之间传输数据。
在前一节中,点的位置是直接编(“硬编码”)写在顶点着色器中的,虽然易于理解但缺乏可扩展性;这一节中,Web程序可以将顶点的位置坐标从JavaScript传到着色器程序中,然后再对应位置上绘制点。在给出代码之前,我们首先讨论一下这种操作的可行性。

目前而言,将位置信息从JavaScript程序中传给顶点着色器,有两种方式:attribute变量和uniform变量。
在这里插入图片描述

  • attribute变量:传输与顶点相关的数据
  • uniform变量:传输那些对于所有顶点都相同(或与顶点无关)的数据

两个变量都是GLSL ES变量,被用来从外部向顶点着色器内传输数据,示例中选用attribute变量,示例代码如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Draw a point (1)</title>
  </head>
  <body onload="main()">
    <canvas id="webgl" width="400" height="400">
      Please use the browser supporting "canvas"
    </canvas>
    <!-- 书中的函数库 -->
    <script src="./libs/webgl-utils.js"></script>
    <script src="./libs/webgl-debug.js"></script>
    <script src="./libs/cuon-utils.js"></script>
    <script src="./js/HelloPoint2.js"></script>
  </body>
</html>
// HelloPoint2.js
// 顶点着色器
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'void main(){\n' +
  ' gl_Position = a_Position;\n' +
  ' gl_PointSize = 10.0;\n' +
  '}\n'
// 片元着色器
var FSHADER_SOURCE =
  'void main() {\n' +
  ' gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n' + // 设置颜色
  '}\n'
// 主函数
function main() {
  // 获取<canvas>元素
  let canvas = document.getElementById('webgl')
  // 获取WebGL上下文
  let gl = canvas.getContext('webgl')
  if (!gl) {
    console.log('Failed to get the rendering context for WebGL')
  }
  // 初始化着色器
  if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
    console.log('Failed to initialize shaders')
    return
  }
  // 获取attribute变量的存储位置
  let a_Position = gl.getAttribLocation(gl.program, 'a_Position')
  if (a_Position < 0) {
    console.log('Failed to get the storage location of a_Position')
  }
  // 将顶点位置传输给attribute变量
  gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0)
  // 设置<canvas>背景色
  gl.clearColor(0.0, 0.0, 0.0, 1.0)
  // 清除<canvas>
  gl.clear(gl.COLOR_BUFFER_BIT)
  // 绘制一个点
  gl.drawArrays(gl.POINTS, 0, 1)
}

关于HelloPoint2.js文件,尤其是新的顶点着色器、attribute变量和由此而来的一系列变化,下面逐步进行解析。


attribute变量的声明与赋值:

在顶点着色器的第一行GLSL ES语言中声明了此变量(第3行):

// 顶点着色器
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'void main(){\n' +
  ' gl_Position = a_Position;\n' +
  ' gl_PointSize = 10.0;\n' +
  '}\n'

attribute变量的声明按照以下的格式:<存储限定符><类型><变量名>
在这里插入图片描述

  • 关键词attribute被称为存储限定符(storage qualifier),它表示接下来的变量(示例中是a_Position)是一个attribute变量。attribute变量必须声明成全局变量,数据将从着色器外部传给该变量。

本书遵循这样一个约定,所有的attribute变量都以a_前缀开始,所有的uniform变量都以u_前缀开始。

  • a_Position的类型是vec4,在顶点着色器的主函数中,它被赋值给gl_Position(第5行),二者类型一致。

至此,着色器已经准备好从外部接收顶点坐标了。


获取attribute变量的存储位置:

在着色器的GLSL ES代码准备好后,与之前的示例一致,开始获取<canvas>元素、获取绘图上下文、调用initShaders()在WebGL系统中初始化着色器。在建立顶点着色器时,WebGL会对着色器进行解析,辨识出其具有的attribute变量。

与一般的JavaScript变量赋值不同,在WebGL系统之外向系统内的变量赋值需要知道该变量的存储地址,再通过存储地址向变量传输数据。获取attribute变量地址的部分代码如下:

  // 获取attribute变量的存储位置
  let a_Position = gl.getAttribLocation(gl.program, 'a_Position')
  if (a_Position < 0) {
    console.log('Failed to get the storage location of a_Position')
  }

此处采用的方法为gl.getAttribLocation(),函数规范如下:

gl.getAttribLocation(program, name):获取由name参数指定的attribute变量的存储地址。
参数:
program: 指定包含顶点着色器和片元着色器的着色器程序对象;
name: 指定想要获取其存储地址的attribute变量的名称;
返回值:
大于等于0:attribute变量的存储地址
-1:指定的attribute变量不存在,或者其命名具有gl_webgl_前缀
错误:
INVALID_OPERATION:程序对象未能成功连接(参见第9章)
INVALID_VALUE:name参数的长度大于attribute变量名的最大长度(默认256字节)

补充:

  • 方法的第一个参数为程序对象,在initShader()之后,程序对象被挂载在了绘图上下文(示例中为gl)下,可通过gl.program进行访问,关于程序对象更多的介绍会在第8章涉及。

  • 方法第二个参数为想要获取存储地址的attribute变量名称

  • 方法返回attribute变量的存储地址

获取a_Position地址的过程如下图所示:

在这里插入图片描述


向attribute变量赋值:

  // 将顶点位置传输给attribute变量
  gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0)

通过存储地址向attribute变量赋值,此处采用上下文挂载的方法gl.vertexAttrib3f()来进行。

笔者没有了解到JavaScript中是否有此类方式,估计没有

函数的规范如下:

gl.vertexAttrib3f(location, v0, v1, v2):将数据(v0, v1, v2)传给由location参数指定的attribute变量。
参数:
location: 指定将要修改的attribute变量的存储位置;
v0: 指定填充attribute变量的第一个分量的值;
v1: 指定填充attribute变量的第二个分量的值;
v2: 指定填充attribute变量的第三个分量的值;
**返回值:**无
错误:
INVALID_OPERATION:没有当前的program对象
INVALID_VALUE:location大于等于attribute变量的最大数目(默认为8)

代码中我们传递了三个浮点数,而a_Position变量得数据类型时vec4,此处该方法会默认将第四个分量设置为1.0。颜色值得第4个分量为1.0表示该颜色完全不透明,而齐次坐标得第4个分量为1.0使齐次坐标和三维坐标对应起来,所以1.0是一个“安全”的第4分量。下一部分对其同族函数的说明中介绍了默认填充的方式。

通过地址向a_Position传值的过程如下图所示:(结合获取地址的示意图,可以更清晰地了解此过程)

在这里插入图片描述

这样,我们就成功地将点的x、y、z坐标值通过JavaScript传入了着色器,而不是静态地写在着色器中。后面的步骤则大致相同:设置背景色、清空绘图区、调用绘图方法根据参数调用着色器中数据进行绘制。


gl.vertexAttrib3f()的同族函数:

gl.vertexAttrib3f()是一系列同族函数中的一个,该系列函数的任务就是从JavaScript向顶点着色器中的attribute变量传值。此处将该系列函数规范罗列如下:

gl.vertexAttrib1f(location, v0);
gl.vertexAttrib2f(location, v0, v1);
gl.vertexAttrib3f(location, v0, v1, v2);
gl.vertexAttrib4f(location, v0, v1, v2, v4):
将数据(v0, v1, v2)传给由location参数指定的attribute变量。
gl.vertexAttrib1f()仅传输一个值,这个值被填充到attribute变量的第1个分量中,第2、3分量将被设为0.0,第4个分量将被设为1.0。
类似的,gl.vertexAttrib2f()将填充前两个分量,第3个分量为0.0,第4个分量为1.0。
gl.vertexAttrib3f()填充前三个分量,第4个分量为1.0。
gl.vertexAttrib4f()填充了所有四个分量。
参数:
location: 指定attribute变量的存储位置;
v0, v2, v3, v4: 指定传输给attribute变量的四个分量的值;
**返回值:**无
错误:
INVALID_VALUE:location大于等于attribute变量的最大数目(默认为8)

这一系列函数还存在矢量版本,可以接收类型化数组(见第4章)。矢量版本的函数在原函数的末尾加了字母‘v’(vector),见如下示例:

var position = new Float32Array([1.0, 2.0, 3.0, 1.0])
gl.vertexAttrib4fv(a_Position, position)

WebGL相关函数命名规范(与OpenGL ES 2.0一致):
<基础函数名><参数个数><参数类型>
在这里插入图片描述

当然,对于接收数组的矢量版本,函数名中的数字代表所接收数组中的元素个数。


灵活使用attribute:

为了增加对attribute的理解,我们可以多做尝试,鉴于此,书中的一个示例对我们很有帮助。

**示例:**通过attribute来传输顶点大小的信息
代码:

  1. 在原代码的基础上,需要修改顶点着色器,如下:
// 顶点着色器
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'attribute float a_PointSize;\n' +
  'void main(){\n' +
  ' gl_Position = a_Position;\n' +
  ' gl_PointSize = a_PointSize;\n' +
  '}\n'
  1. 获取<canvas>元素,获取WebGL上下文,初始化着色器
  2. 获取attribute变量a_PointSize的存储地址
  let a_PointSize = gl.getAttribLocation(gl.program, 'a_PointSize')
  if (a_PointSize < 0) {
    console.log('Failed to get the storage location of a_Position')
  }
  1. 通过gl.vertexAttrib1f()传输一个浮点数给a_PointSize变量
  // 将顶点大小传输给attribute变量
  gl.vertexAttrib1f(a_PointSize, 10.0)
  1. 设置背景色、清空绘图区、绘制一个点

通过鼠标点击绘点(通过鼠标事件获取坐标)

相关内容:在attribute变量传递参数的基础上,通过JavaScript获取鼠标事件的坐标,再经过坐标转换传递给attribute变量;Web颜色缓冲区每次绘制之后都会重置
相关函数:JavaScript鼠标事件onmousedown/onmouseup/onclick

小结:此示例算是前端知识与WebGL系统一定程度上的结合,通过前端技术获取点击坐标,传递给WebGL系统进行绘制。

问题:怎么判断绘制结束呢?此处没出现老openGL中的gl.begin()和gl.end(),也许代码结束运行就是结束?希望在书的后面有相关解释。

该节通过ClickedPoint示例,展示了在鼠标点击的位置绘制出点的方法,更灵活地扩展JavaScript传输数据到顶点着色器的能力。示例运行后,结果如下(点击了4次):

该示例使用事件响应函数来处理鼠标事件,示例代码如下:

// ClickedPoints.js
// 顶点着色器
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'void main() {\n' +
  ' gl_Position = a_Position;\n' +
  ' gl_PointSize = 10.0;\n' +
  '}\n'
// 片元着色器
var FSHADER_SOURCE =
  'void main() {\n' +
  ' gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n' + // 设置颜色
  '}\n'
// 主函数
function main() {
  // 获取<canvas>元素
  let canvas = document.getElementById('webgl')
  // 获取WebGL绘图上下文
  let gl = getWebGLContext(canvas)
  if (!gl) {
    console.log('Failed to get the rendering context for WebGL')
    return
  }
  // 初始化着色器
  if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
    console.log('Failed to initialize shaders')
    return
  }
  // 获取a_Position变量的存储位置
  let a_Position = gl.getAttribLocation(gl.program, 'a_Position')
  if (a_Position < 0) {
    console.log('Failed to get the storage location of a_Position')
  }
  // 注册鼠标点击事件响应函数
  canvas.onmousedown = function (ev) {
    // console.log(ev)
    click(ev, gl, canvas, a_Position)
  }
  // 设置背景色
  gl.clearColor(0.0, 0.0, 0.0, 1.0)
  // 清空绘图区域
  gl.clear(gl.COLOR_BUFFER_BIT)
}
var g_points = [] // 鼠标点击位置数组
function click(ev, gl, canvas, a_Position) {
  let x = ev.clientX // 鼠标点击处的x坐标
  let y = ev.clientY // 鼠标点击处的y坐标
  let rect = ev.target.getBoundingClientRect() //cnavas边界坐标
  // 坐标转换
  x = (x - rect.left - canvas.width / 2) / (canvas.width / 2)
  y = (canvas.height / 2 - (y - rect.top)) / (canvas.height / 2)
  // 将坐标存储到g_points数组中
  g_points.push(x)
  g_points.push(y)

  // 清空绘图区
  gl.clear(gl.COLOR_BUFFER_BIT)
  // 绘制数组中的点
  let len = g_points.length
  for (let i = 0; i < len; i += 2) {
    // 将点的位置传递到变量中a_Position
    gl.vertexAttrib3f(a_Position, g_points[i], g_points[i + 1], 0.0)
    // 绘制点
    gl.drawArrays(gl.POINTS, 0, 1)
  }
}

相比于上一个示例,此处将x和y坐标通过JavaScript自带的鼠标点击事件来获取,通过坐标转换传递到着色器中。下面初步分析。


注册事件响应函数:

// 注册鼠标点击事件响应函数
  canvas.onmousedown = function (ev) {
    // console.log(ev)
    click(ev, gl, canvas, a_Position)
  }

之前的步骤——获取<canvas>元素、获取WebGL上下文、初始化着色器、获取attribute变量存储位置——和HelloPoint2.js相同,最大的不同就在于事件响应函数:

  • 事件响应函数注册在<canvas>的onmousedown事件上,代表在段落上按下鼠标按键的事件,类似的鼠标事件还有onmouseup和onclick,分别是抬起鼠标按键和点击按键。这一部分属于前端的DOM事件中的MouseEvent,感兴趣的学者可以查找相关的DOM事件进行更多了解。
  • canvas.onmousedown被赋予了一段匿名函数作为响应函数,故名思意,匿名函数是不需要命名的函数,在JavaScript中应用较多,能简化程序的编写流程。感兴趣的学者可以查找更多JavaScript匿名函数的内容进行了解。
  • 挂载完成后,在<canvas>标签内触发上述事件(包括其他响应事件)后,会自动传递事件对象至响应函数,此处用ev作为函数的形参接收,ev中挂载了许多关于这一事件的属性,包括触发对象(target)、坐标等,不同事件传递的事件对象不同,可以通过console.log()方法打印在控制台中进行查看,感兴趣的学者也可以从网上查找更完整的信息。
  • 在这一示例中匿名函数调用了click()函数,传递了ev(事件对象)、gl(WebGL上下文)、canvas(html标签)和a_Position(attribute变量存储地址)四个参数给该函数。这也是匿名函数的常用用法:方便独立的函数调用参数。此处click()函数是处理事件的主要函数。

响应鼠标点击事件:click()函数

click()函数的大体思路如下:

  1. 获取鼠标点击的位置存储在一个数组中;
  2. 清空<canvas>
  3. 根据数组的每个元素,在相应的位置绘制点。

之后逐步进行分析:

  • 获取坐标及坐标转换。
  let x = ev.clientX // 鼠标点击处的x坐标
  let y = ev.clientY // 鼠标点击处的y坐标
  let rect = ev.target.getBoundingClientRect() //cnavas边界坐标
  // 坐标转换
  x = (x - rect.left - canvas.width / 2) / (canvas.width / 2)
  y = (canvas.height / 2 - (y - rect.top)) / (canvas.height / 2)

书中坐标转换的代码似乎给错了,此处笔者已经修改

click()函数中坐标获取和转换的代码如上所示,相关方法介绍如下:

  1. 鼠标点击的位置信息存储在事件对象ev中,这里可以通过访问en.clientXen.clientY来获取坐标。点击事件是JavaScript中自带的事件,获取的坐标自然也是“浏览器客户区”(client area)中的坐标;

  2. 浏览器客户区坐标、canvas标签坐标和WebGL坐标均不相同,所以我们需要将上面获取的浏览器客户区坐标转换为我们可以直接传输的WebGL坐标,过程如下:

    1. 获取<canvas>标签区域在浏览器中的坐标,为代码第4行的内容,rect.left和rect.top分别是<canvas>标签左边界和上边界在浏览器中的位置,如此 ( x − r e c t . l e f t ) (x-rect.left) (xrect.left) ( x − r e c t . t o p ) (x-rect.top) (xrect.top)分别是原坐标转换为<canvas>坐标系下的x和y坐标;

    在这里插入图片描述

    1. 通过canvas.heightcnavas.width可以获取<canvas>标签的高度和宽度,其中心点坐标则为 ( c a n v a s . h e i g h t / 2 , c a n v a s . w i d t h / 2 ) (canvas.height/2,canvas.width/2) (canvas.height/2,canvas.width/2),可以使用 ( ( x − r e c t . l e f t ) − c a n v a s . w i d t h / 2 ) ((x-rect.left)-canvas.width/2) ((xrect.left)canvas.width/2) ( c a n v a s . h e i g h t / 2 − ( y − r e c t . t o p ) ) (canvas.height/2-(y-rect.top)) (canvas.height/2(yrect.top))<canvas>坐标系的原点平移到中心点(WebGL坐标系统原点的位置),再经过放缩,获得原坐标的WebGL坐标的x和y坐标分别为 ( ( x − r e c t . l e f t ) − c a n v a s . w i d t h / 2 ) ( c a n v a s . w i d t h / 2 ) \frac{((x-rect.left)-canvas.width/2)}{(canvas.width/2)} (canvas.width/2)((xrect.left)canvas.width/2) ( c a n v a s . h e i g h t / 2 − ( y − r e c t . t o p ) ) ( c o n v a s . h e i g h t / 2 ) \frac{(canvas.height/2-(y-rect.top))}{(convas.height/2)} (convas.height/2)(canvas.height/2(yrect.top))

    在这里插入图片描述

  • 坐标转换后的点存入全局变量g_points
  // 将坐标存储到g_points数组中
  g_points.push(x)
  g_points.push(y)

此处将转换后的坐标添加到了g_points数组的末尾。

为什么把每次点击的位置都记录下来统一绘制,而不是仅记录最近一次鼠标点击位置单独绘制?这是因为WebGL使用了颜色缓冲区。
每次绘制操作都是在颜色缓冲区中进行,绘制结束后将缓冲区的内容显示在屏幕上,然后颜色缓冲区就会重置,其中的内容会丢失。
所以,此处需要把所有的点都保存下来以统一绘制(遍历),在绘制之前需要进行“清空绘图区”操作。

但是,怎么判断绘制结束呢?此处没出现老openGL中的gl.begin()和gl.end(),也许代码结束运行就是结束?希望在书的后面有相关解释。

  • 清空绘图区,遍历数组绘制点

此处g_points是声明在主函数之前的全局变量,挂载在document之下。可以在控制台中直接输入g_points进行访问。

  // 清空绘图区
  gl.clear(gl.COLOR_BUFFER_BIT)
  // 绘制数组中的点
  let len = g_points.length
  for (let i = 0; i < len; i += 2) {
    // 将点的位置传递到变量中a_Position
    gl.vertexAttrib3f(a_Position, g_points[i], g_points[i + 1], 0.0)
    // 绘制点
    gl.drawArrays(gl.POINTS, 0, 1)
  }

用示例程序做实验:

书中这一部分用示例程序ClickedPoints.js做了两个小实验,以增进对WebGL过程的理解。如果读懂了上面所记录的部分,下面的试验很容易就猜出结果。

  1. 在click()函数中去掉清空绘图区的部分gl.clear()。

​ 此时在没点击的时候,绘图区背景是黑色,点击之后,背景变成了白色。因为绘制点之后,颜色缓冲区就被WebGL重置为了默认颜色(0.0, 0.0, 0.0, 0.0)。

  1. 在g_points中将x和y作为一个组合存储。

​ 此方法能增加程序的可读性,实际效果相同


改变点的颜色与uniform变量(动态设置片元着色器)

相关内容:JavaScript设置点的颜色;JavaScript传递点的颜色给片元着色器(通过uniform变量)
相关函数:gl.getUniformLocation(), gl.uniform4f()

小结:该示例展示了uniform相关内容,其用法与attribute类似,但适用范围不同:attribute只能用在顶点着色器中其与单个顶点相关、uniform与单个顶点无关(各个顶点都是一致的)。所谓动态设置着色器,除了通过JavaScript传递参数进入WebGL系统外,动态生成颜色采用的是JavaScript本身的能力,是不是WebGL系统内不能动态生成颜色?留待后续学习。

该示例在ClickedPoints.js的基础上,动态设置点的颜色,使其在第一象限为红色,第三象限为蓝色,其他位置为白色。示例效果如下:

在这里插入图片描述

为了动态设置顶点着色器,我们引入了attribute变量,但可惜的是,似乎只有顶点着色器可以使用attribute变量,动态设置片元着色器时,我们需要采用uniform变量。当然,varying变量也可以用于片元着色器,但其使用比较复杂,书中放在第5章开始讲解。

uniform变量的使用与attribute变量类似,同样是先声明变量,然后赋值,最后通过JavaScript向该变量传值。前文说过,attribute变量传输与顶点“相关”的数据,uniform变量所有顶点都相同或称与顶点“无关”、顶点着色器和片元着色器“一致的”数据。下面我们看一下示例代码中的用法:

// ColorPoints.js
// 顶点着色器
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'void main() {\n' +
  ' gl_Position = a_Position;\n' +
  ' gl_PointSize = 10.0;\n' +
  '}\n'
// 片元着色器
var FSHADER_SOURCE =
  'precision mediump float;\n' +
  'uniform vec4 u_FragColor;\n' + // uniform变量
  'void main() {\n' +
  ' gl_FragColor = u_FragColor;\n' +
  '}\n'
// 主函数
function main() {
  // 获取<canvas>元素
  let canvas = document.getElementById('webgl')
  // 获取webgl上下文
  let gl = getWebGLContext(canvas)
  if (!gl) {
    console.log('Failed to get the rendering context for WebGL')
    return
  }
  // 初始化着色器
  if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
    console.log('Failed to initialize shaders')
    return
  }
  // 获取a_Position变量的存储位置
  let a_Position = gl.getAttribLocation(gl.program, 'a_Position')
  if (a_Position < 0) {
    console.log('Failed to get the storage location of a_Position')
  }
  // 获取u_FragColor变量的存储位置
  let u_FragColor = gl.getUniformLocation(gl.program, 'u_FragColor')
  if (!u_FragColor) {
    console.log('Failed to get the storage location of u_FragColor')
  }
  // 注册鼠标响应函数
  canvas.onmousedown = function (ev) {
    click(ev, gl, canvas, a_Position, u_FragColor)
  }
  // 设置背景颜色
  gl.clearColor(0.0, 0.0, 0.0, 1.0)
  // 清空绘图区
  gl.clear(gl.COLOR_BUFFER_BIT)
}

var g_points = [] // 鼠标点击位置数组
var g_colors = [] // 存储点颜色的数组
// 鼠标点击响应函数主体
function click(ev, gl, canvas, a_Position, u_FragColor) {
  let x = ev.clientX // 鼠标点击处的x坐标
  let y = ev.clientY // 鼠标点击处的y坐标
  let rect = ev.target.getBoundingClientRect() // canvas标签边界
  // 坐标转换
  x = (x - rect.left - canvas.width / 2) / (canvas.width / 2)
  y = (canvas.height / 2 - (y - rect.top)) / (canvas.height / 2)
  // 将坐标存储到g_points中
  g_points.push([x, y])
  // 将点的颜色存储到g_colors中
  if (x >= 0.0 && y >= 0.0) {
    // 第一象限
    g_colors.push([1.0, 0.0, 0.0, 1.0]) // 红色
  } else if (x < 0.0 && y < 0.0) {
    // 第三象限
    g_colors.push([0.0, 0.0, 1.0, 1.0]) // 绿色
  } else {
    // 其他
    g_colors.push([1.0, 1.0, 1.0, 1.0]) // 白色
  }

  // 清空<canvas>
  gl.clear(gl.COLOR_BUFFER_BIT)
  // 遍历数组,开始绘制
  let len = g_points.length
  for (let i = 0; i < len; i++) {
    let xy = g_points[i]
    let rgba = g_colors[i]

    // 将点的位置传递到a_Position变量中
    // gl.vertexAttrib3f(a_Position, xy[0], xy[1], 0.0)
    gl.vertexAttrib2fv(a_Position, xy)
    // 将点的颜色传输到u_FragColor变量中
    // gl.uniform4f(u_FragColor, rgba[0], rgba[1], rgba[2], rgba[3])
    gl.uniform4fv(u_FragColor, rgba)
    // 绘制点
    gl.drawArrays(gl.POINTS, 0, 1)
  }
}

在传递点的位置和颜色时,书中没有选用矢量方法,笔者在此处改为矢量方法,新方法更具易读性。书中采用的函数注释在新函数的上面,供大家参考。

在JavaScript和WebGL交互之外,示例中采用了“自己设计”的方式,先计算出应得的颜色值,在将颜色值传递给WebGL系统。以下将着重说明示例中uniform部分的内容。


uniform变量

示例中片元着色器使用了uniform变量,着色器部分代码如下:

// 片元着色器
var FSHADER_SOURCE =
  'precision mediump float;\n' +
  'uniform vec4 u_FragColor;\n' + // uniform变量
  'void main() {\n' +
  ' gl_FragColor = u_FragColor;\n' +
  '}\n'

uniform变量用来从JavaScript程序向顶点着色器和片元着色器传输“一致的”(不变的)数据。与attribute变量类似,uniform变量同样需要声明赋值等步骤。

  • uniform变量的声明遵循与attribute变量相同的格式<存储限定符><类型><变量名>

在这里插入图片描述

  • 示例中,uniform变量u_FragColor被赋值给gl_FragColor变量(第6行),二者数据类型一致(vec4)
  • 示例使用了精度限定词(precision qualifier)来指定变量的范围和精度,第3行precision mediump float意为中等精度,关于这一部分会在第5章详细讨论。

获取uniform变量的存储地址

  // 获取u_FragColor变量的存储位置
  let u_FragColor = gl.getUniformLocation(gl.program, 'u_FragColor')
  if (!u_FragColor) {
    console.log('Failed to get the storage location of u_FragColor')
  }

gl.getUniformLocation(program, name):获取指定名称的uniform变量的存储位置。
参数:
program: 指定包含顶点着色器和片元着色器的着色器程序对象;
name: 指定想要获取其存储位置的uniform变量名称;
返回值:
non-null:指定uniform变量的位置
null:指定的uniform变量不存在,或者其命名具有gl_webgl_前缀
错误:
INVALID_OPERATION:程序对象未能成功连接(参见第9章)
INVALID_VALUE:name参数的长度大于uniform变量名的最大长度(默认256字节)

该函数与gl.getAttribLocation()最大的区别为:指定变量不存在时,返回的是null而不是-1。所以在检查的时候,需要用’!‘(取反)操作符而不是’<0’


向uniform变量赋值

attribute变量的设置与上一个示例相同,此处主要说明uniform相关内容。

这一部分在click()中,首先通过JavaScript构建了一个规则来动态设置点的颜色:第一象限为红色、第三象限为绿色、其他为白色。规则代码如下:

  // 将点的颜色存储到g_colors中
  if (x >= 0.0 && y >= 0.0) {
    // 第一象限
    g_colors.push([1.0, 0.0, 0.0, 1.0]) // 红色
  } else if (x < 0.0 && y < 0.0) {
    // 第三象限
    g_colors.push([0.0, 0.0, 1.0, 1.0]) // 绿色
  } else {
    // 其他
    g_colors.push([1.0, 1.0, 1.0, 1.0]) // 白色
  }

随后则是清空绘图区,逐点输入坐标和颜色进行绘制。示例中采用WebGL函数gl.uniform4f()向变量中写入数据,函数规范如下:

gl.uniform4f(location, v0, v1, v2, v3):获取指定名称的uniform变量的存储位置。
参数:
location: 指定将要修改的uniform变量的存储位置;
v0: 指定填充uniform变量的第一个分量的值;
v1: 指定填充uniform变量的第二个分量的值;
v2: 指定填充uniform变量的第三个分量的值;
**返回值:**无
错误:
INVALID_OPERATION:没有当前的program对象,或者location是非法的变量存储地址

逐点绘制得代码如下:

  // 清空<canvas>
  gl.clear(gl.COLOR_BUFFER_BIT)
  // 遍历数组,开始绘制
  let len = g_points.length
  for (let i = 0; i < len; i++) {
    let xy = g_points[i]
    let rgba = g_colors[i]

    // 将点的位置传递到a_Position变量中
    // gl.vertexAttrib3f(a_Position, xy[0], xy[1], 0.0)
    gl.vertexAttrib2fv(a_Position, xy)
    // 将点的颜色传输到u_FragColor变量中
    // gl.uniform4f(u_FragColor, rgba[0], rgba[1], rgba[2], rgba[3])
    gl.uniform4fv(u_FragColor, rgba)
    // 绘制点
    gl.drawArrays(gl.POINTS, 0, 1)
  }

gl.uniform4f()的同族函数

gl.uniform1f(location, v0);
gl.uniform2f(location, v0, v1);
gl.uniform3f(location, v0, v1, v2);
gl.uniform4f(location, v0, v1, v2, v4):
将数据传输给由location参数指定的uniform变量。
gl.uniform1f()仅传输一个值,这个值被填充到attribute变量的第一个分量中,第二、三分量将被设为0.0,第四个分量将被设为1.0。
类似的,gl.uniform2f()将填充前两个分量,第三个分量为0.0,第四个分量为1.0。
gl.uniform3f()填充前三个分量,第四个分量为1.0。
gl.uniform4f()填充了所有四个分量。
参数:
location: 指定uniform变量的存储位置;
v0, v2, v3, v4: 指定传输给uniform变量的四个分量的值;
**返回值:**无
错误:
INVALID_OPERATION:没有当前的program对象,或者location是非法的变量存储地址

gl.vertexAttrib4f()同族函数类似,这一系列函数在末尾加上’v’也可以获得其矢量版本。


总结

  1. 着色器初识
  2. JavaScript传递数据给着色器

关键:顶点着色器进行的是逐顶点的操作,片元着色器进行的是逐片元的操作。

本以为学的会很快,可是这么长时间才总结完第二章,后面还需再接再厉啊!

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值