基础概念 | 初始化代码

是什么?

  • 它仅仅是一个光栅化引擎,能绘制出点,线和三角形
  • 在电脑的GPU中运行,所以代码需要满足一定要求(HTML <canvas> element)
    • 需要提供成对的方法
    • 每对方法中一个叫顶点着色器, 另一个叫片断着色器,并且使用一种和C或C++类似的强类型的语言 GLSL
    • 每一对组合起来称作一个 program(着色程序)
    • 顶点着色器的作用:计算顶点的位置。根据计算出的一系列顶点位置
    • 片断着色器的作用:计算出当前绘制图元中每个像素的颜色值。

着色器获取数据的4种方法

  1. 属性(Attributes)和缓冲 缓冲是发送到GPU的一些二进制数据序列,通常情况下缓冲数据包括位置,法向量,纹理坐标,顶点颜色值等。 你可以存储任何数据。 属性用来指明怎么从缓冲中获取所需数据并将它提供给顶点着色器。 例如你可能在缓冲中用三个32位的浮点型数据存储一个位置值。 对于一个确切的属性你需要告诉它从哪个缓冲中获取数据,获取什么类型的数据(三个32位的浮点数据), 起始偏移值是多少,到下一个位置的字节数是多少。 缓冲不是随意读取的。事实上顶点着色器运行的次数是一个指定的确切数字, 每一次运行属性会从指定的缓冲中按照指定规则依次获取下一个值。

  2. 全局变量(Uniforms) 全局变量在着色程序运行前赋值,在运行过程中全局有效。

  3. 纹理(Textures) 纹理是一个数据序列,可以在着色程序运行中随意读取其中的数据。 大多数情况存放的是图像数据,但是纹理仅仅是数据序列, 你也可以随意存放除了颜色数据以外的其它数据。

  4. 可变量(Varyings) 可变量是一种顶点着色器给片断着色器传值的方式,依照渲染的图元是点, 线还是三角形,顶点着色器中设置的可变量会在片断着色器运行中获取不同的插值。

例子

WebGL只关心两件事:裁剪空间中的坐标值和颜色值

无论你的画布有多大,裁剪空间的坐标范围永远是 -1 到 1

  1. 创建两个着色器方法
  2. HTML中的canvas(画布)对象
    • var canvas = document.getElementById(“c”); 获取它
  3. 创建一个WebGL渲染上下文(WebGLRenderingContext)

    var gl = canvas.getContext(“webgl”);
    if (!gl) {
     // 你不能使用WebGL!
     …
    
  4. 编译着色器对然后提交到GPU

    • 先让我们通过字符串获取它们(可以利用JavaScript中创建字符串的方式创建GLSL字符串 / 用串联的方式(concatenating), 用AJAX下载,用多行模板数据 / 将它们放在非JavaScript类型的标签中)

      <script id="2d-vertex-shader" type="notjs">
        
      // 一个属性变量,将会从缓冲中获取数据
      attribute vec4 a_position;
      
      // 所有着色器都有一个main方法
      void main() {
      
      // gl_Position 是一个顶点着色器主要设置的变量
      gl_Position = a_position;
      }
        
      </script>
        
      <script id="2d-fragment-shader" type="notjs">
        
      // 片断着色器没有默认精度,所以我们需要设置一个精度
      // mediump是一个不错的默认值,代表“medium precision”(中等精度)
      precision mediump float;
      
      void main() {
      // gl_FragColor是一个片断着色器主要设置的变量
      gl_FragColor = vec4(1, 0, 0.5, 1); // 返回“瑞迪施紫色”
      }
        
      </script>
      
      
    • 创建一个着色器,只需要上传GLSL数据,然后编译成着色器

      // 创建着色器方法,输入参数:渲染上下文,着色器类型,数据源
      function createShader(gl, type, source) {
      var shader = gl.createShader(type); // 创建着色器对象
      gl.shaderSource(shader, source); // 提供数据源
      gl.compileShader(shader); // 编译 -> 生成着色器
      var success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
      if (success) {
      return shader;
      }
        
      console.log(gl.getShaderInfoLog(shader));
      gl.deleteShader(shader);
      }
      
    • 使用以上方法创建两个着色器

      var vertexShaderSource = document.getElementById("2d-vertex-shader").text;
      var fragmentShaderSource = document.getElementById("2d-fragment-shader").text;
        
      var vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
      var fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
      
      
  5. 将这两个着色器 link(链接)到一个 program(着色程序)

    function createProgram(gl, vertexShader, fragmentShader) {
    var program = gl.createProgram();
    gl.attachShader(program, vertexShader);
    gl.attachShader(program, fragmentShader);
    gl.linkProgram(program);
    var success = gl.getProgramParameter(program, gl.LINK_STATUS);
    if (success) {
    return program;
    }
      
    console.log(gl.getProgramInfoLog(program));
    gl.deleteProgram(program);
    }
    然后调用它
    var program = createProgram(gl, vertexShader, fragmentShader);
    
  6. 现在我们已经在GPU上创建了一个GLSL着色程序,我们还需要给它提供数据。

    • 在这个例子中GLSL着色程序的唯一输入是一个属性值 a_position。 我们要做的第一件事就是从刚才创建的GLSL着色程序中找到这个属性值所在的位置(寻找属性值位置(和全局属性位置)应该在初始化的时候完成,而不是在渲染循环中)var positionAttributeLocation = gl.getAttribLocation(program, "a_position");

    • 创建一个缓冲 var positionBuffer = gl.createBuffer();

    • WebGL可以通过绑定点操控全局范围内的许多数据,你可以把绑定点想象成一个WebGL内部的全局变量。 首先绑定一个数据源到绑定点,然后可以引用绑定点指向该数据源。 所以让我们来绑定位置信息缓冲(下面的绑定点就是ARRAY_BUFFER)gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);

    • 通过绑定点向缓冲中存放数据

      		// 三个二维点坐标
      		var positions = [
      		  0, 0,
      		  0, 0.5,
      		  0.7, 0,
      		];
      		gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
      
      

对于 gl.bufferData 这一函数的理解

  1. 第一件事是我们有了一个JavaScript序列positions。然而WebGL需要强类型数据,所以new Float32Array(positions)创建了32位浮点型数据序列,并从positions中复制数据到序列中,然后gl.bufferData复制这些数据到GPU的positionBuffer对象上。
  2. 它最终传递到positionBuffer上是因为在前一步中我们我们将它绑定到了ARRAY_BUFFER(也就是绑定点)上。
  3. 最后一个参数gl.STATIC_DRAW是告诉WebGL我们将怎么使用这些数据,根据这个提示WebGL会做出一些优化。 gl.STATIC_DRAW提示WebGL我们不会经常改变这些数据。

接着上面这个例子

在绘制之前我们应该调整画布(canvas)的尺寸以匹配它的显示尺寸。画布就像图片一样有两个尺寸。 一个是它拥有的实际像素个数,另一个是它显示的大小。CSS决定画布显示的大小。为了使画布的像素数和显示大小匹配, 我这里使用了一个辅助方法,你可以在这里获取更多相关信息

webglUtils.resizeCanvasToDisplaySize(gl.canvas);
// 这样就告诉WebGL裁剪空间的 -1 -> +1 分别对应到x轴的 0 -> gl.canvas.width 和y轴的 0 -> gl.canvas.height。
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
// 清空画布 让画布变透明了。
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
// 告诉它用我们之前写好的着色程序(一个着色器对)
gl.useProgram(program);

// 告诉WebGL怎么从我们之前准备的缓冲中获取数据给着色器中的属性
gl.enableVertexAttribArray(positionAttributeLocation);
// 将绑定点绑定到缓冲数据(positionBuffer)
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
 
// 告诉属性怎么从positionBuffer中读取数据 (ARRAY_BUFFER)
var size = 2;          // 每次迭代运行提取两个单位数据
var type = gl.FLOAT;   // 每个单位的数据类型是32位浮点型
var normalize = false; // 不需要归一化数据
var stride = 0;        // 0 = 移动单位数量 * 每个单位占用内存(sizeof(type))
                       // 每次迭代运行运动多少内存到下一个数据开始点
var offset = 0;        // 从缓冲起始位置开始读取
gl.vertexAttribPointer(
    positionAttributeLocation, size, type, normalize, stride, offset)
var primitiveType = gl.TRIANGLES;
var offset = 0;
var count = 3;
gl.drawArrays(primitiveType, offset, count);

// 让WebGL运行我们的GLSL着色程序了
var primitiveType = gl.TRIANGLES;
var offset = 0;
var count = 3;
gl.drawArrays(primitiveType, offset, count);

从GLSL的顶点着色器中注意到a_position属性的数据类型是vec4,它是一个有四个浮点数据的数据类型。在JavaScript中你可以把它想象成 a_position = {x: 0, y: 0, z: 0, w: 0}。之前我们设置的size = 2, 属性默认值是0, 0, 0, 1,然后属性将会从缓冲中获取前两个值( x 和 y )。 z和w还是默认值 0 和 1 。

因为count = 3,所以顶点着色器将运行三次。 第一次运行将会从位置缓冲中读取前两个值赋给属性值a_position.xa_position.y。 第二次运行a_position.xy将会被赋予后两个值,最后一次运行将被赋予最后两个值。