# webGL 基础

我们将现实世界中绘制到屏幕上,需要进行两次坐标的变换:

avatar

整个 webgl 渲染流程:

avatar

# webGL 程序的结构

在 HTML 中,动态网页包含 HTML 和 JavaScript 两种语言,引入 webgl 后,还需要加入着色器语言 GLSL ES。 avatar

我们是通过对 GPU 进行编程,进行渲染图形:对于 GPU 编程主要从下面两个方面进行编程:

  • webgl:API glTranslate/glRotatef
  • GLSL:顶点着色器 - gl_Position 和 片元着色器 - gl_FragColor

# webgl 渲染管线

JavaScript 通过显存的 Buffer 来实现顶点着色器与片元着色器的数据传递。 avatar

frame Buffer 是帧缓冲区,保存顶点着色器与片元着色器渲染后的结果,可以进行进一步的操作。

# 渲染过程

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

avatar

  1. 传入坐标
  2. 计算显示坐标
  3. 图元装备(根据坐标,组成图形)
  4. 将装备好的图元变成像素的形式;(顶点着色器)
  5. 片元着色器对每一个像素点进行渲染

顶点着色器的作用:将逻辑上的传入的顶点坐标,根据空间上的变换,进行计算屏幕上展示点的坐标。

# GLSL

  • OpenGL 着色器语言
  • 让开发者可以对渲染过程拥有更多的控制

avatar

GLSL 参考:https://colin1994.github.io/2017/11/11/OpenGLES-Lesson04/

# 存储限定符

对于着色器语言,具有三种数据类型:

  • attribute:只能在 vertex shader 中使用的变量,一般用于传递顶点数据;
  • uniform:常量,不能被 shader 修改,uniform 变量在 vertex 和 fragment 两者之间声明方式完全一致,则他可以在 vertex 和 fragment 共享使用,相当于一个被 vertex 和 fragment shader 共享的全局变量。
  • varying:varying 变量是vertex 和 fragment shader 之间做数据传递用的。

如下代码,声明一个背景色为红色的三角形:

var VSHADER_SOURCE, FSHADER_SOURCE

VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' + 
  'uniform mat4 u_ModelMatrix;\n' +
  'uniform mat4 u_ViewMatrix;\n' +
  'uniform mat4 u_ProjectionMatrix;\n' + 
  'void main () {\n' + 
    'gl_Position = u_ProjectionMatrix * u_ViewMatrix * u_ModelMatrix * a_Position;\n' + 
  '}\n'

FSHADER_SOURCE =
  'void main () {\n' + 
    'gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n' + 
  '}\n'

也可以直接写在页面 script 标签中:

    <script type="notjs" id="vertex-shader">
      # 传入坐标 vec4 是代表一个 4x4 的浮点型矩阵
      attribute vec4 a_Position;

      uniform mat4 u_ModelMatrix;
      uniform mat4 u_ViewMatrix;
      uniform mat4 u_ProjectionMatrix;
      void main() {
        # 顶点位置-webgl 内置 API
        gl_Position = u_ProjectionMatrix * u_ViewMatrix * u_ModelMatrix * a_Position;
      }
    </script>
    <script type="notjs" id="fragment-shader">
      void main() {
        # 设置背景色
        gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
      }
    </script> 

然后通过 dom 操作获取内容:

  const vertexShaderSource = document.getElementById('vertex-shader').text
  const fragShaderSource = document.getElementById('fragment-shader').text

# 其他

  • 精度限定 使用精度限定词来指定变量的范围(最大值和最小值)和精度,下面代码表示中等精度:
precision mediump float;

# webgl 坐标系统

遵循右手法则:

avatar avatar

# webgl 机制 —— 缓冲区对象(buffer object)

我们绘制一个点的时候是不需要用到缓冲区对象:

// 顶点着色器程序
var VSHADER_SOURCE = 
  'void main() {\n' +
  '  gl_Position = vec4(0.0, 0.0, 0.0, 1.0);\n' + // Set the vertex coordinates of the point
  '  gl_PointSize = 10.0;\n' +                    // Set the point size
  '}\n';

// 片元着色器程序
var FSHADER_SOURCE =
  'void main() {\n' +
  '  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n' + // Set the point color
  '}\n';

function main() {
  // 获取 canvas 元素
  var canvas = document.getElementById('webgl');

  // 获取 WebGL 绘图上下文
  var gl = getWebGLContext(canvas);
  if (!gl) {
    console.log('Failed to get the rendering context for WebGL');
    return;
  }
  // 创建 shader 对象
  function loadShader(gl, type, source) {
    // Create shader object
    var shader = gl.createShader(type);
    if (shader == null) {
      console.log('unable to create shader');
      return null;
    }

    // Set the shader program
    gl.shaderSource(shader, source);

    // Compile the shader
    gl.compileShader(shader);

    // Check the result of compilation
    var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
    if (!compiled) {
      var error = gl.getShaderInfoLog(shader);
      console.log('Failed to compile shader: ' + error);
      gl.deleteShader(shader);
      return null;
    }

    return shader;
  }
  // 创建程序
  function createProgram(gl, vshader, fshader) {
    // Create shader object
    var vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader);
    var fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader);
    if (!vertexShader || !fragmentShader) {
      return null;
    }

    // Create a program object
    var program = gl.createProgram();
    if (!program) {
      return null;
    }

    // Attach the shader objects
    gl.attachShader(program, vertexShader);
    gl.attachShader(program, fragmentShader);

    // Link the program object
    gl.linkProgram(program);

    // Check the result of linking
    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(fragmentShader);
      gl.deleteShader(vertexShader);
      return null;
    }
    return program;
  }
  // 初始化着色器方法
  function initShaders(gl, vshader, fshader) {
    var program = createProgram(gl, vshader, fshader);
    if (!program) {
      console.log('Failed to create program');
      return false;
    }

    gl.useProgram(program);
    gl.program = program;

    return true;
  }
  // 初始化着色器
  if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
    console.log('Failed to intialize 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);
}

我们只需要一个 gl.drawArrays(gl.POINTS, 0, 1) 进行每次绘制一个点,但是如果我们需要绘制多个点,就需要用到缓冲区对象,他可以一次性向着色器传入多个顶点的数据,缓冲区对象是 webgl 系统中的一块内存区域,我们可以一次性地向缓冲区对象中填充大量的顶点数据,然后将这些数据保存在其中,供顶点着色器使用。

下面代码是绘制一个三角形并增加旋转动画:

var canvas = document.getElementById('myCanvas')
// 获取 webgl 绘图上下文
var gl = canvas.getContext('webgl')
// 一个程序,可以绑定顶点着色器跟片源着色器
var program = gl.createProgram()

var VSHADER_SOURCE, FSHADER_SOURCE
// 顶点着色器程序
VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' + 
  'uniform mat4 u_ModelMatrix;\n' +
  'uniform mat4 u_ViewMatrix;\n' +
  'uniform mat4 u_ProjectionMatrix;\n' + 
  'void main () {\n' + 
    'gl_Position = u_ProjectionMatrix * u_ViewMatrix * u_ModelMatrix * a_Position;\n' + 
  '}\n'
// 片元着色器程序
FSHADER_SOURCE =
  'void main () {\n' + 
    'gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n' + 
  '}\n'

var vertexShader, fragmentShader

function createShader (gl, sourceCode, type) {
  // create shader
  var shader = gl.createShader(type)
  gl.shaderSource(shader, sourceCode)
  // 编译 shader
  gl.compileShader(shader)
  return shader
}

// 定义顶点着色器
vertexShader = createShader(gl, VSHADER_SOURCE, gl.VERTEX_SHADER)
// 定义片段着色器
fragmentShader = createShader(gl, FSHADER_SOURCE, gl.FRAGMENT_SHADER)

// 和 shader 绑定
gl.attachShader(program, vertexShader)
gl.attachShader(program, fragmentShader)

// 连接 program
gl.linkProgram(program)
gl.useProgram(program)
gl.program = program

var currentAngle = 0
var g_last = Date.now()

var tick = function () {
  // update the new rotation angle
  animate()
  // draw
  draw()
  requestAnimationFrame(tick)
}

function initVertexBuffers (gl) {
  // 定义顶点
  var vertices = new Float32Array([
    0, 0.5, -0.5, -0.5, 0.5, -0.5
  ])
  // 点的个数
  var n = 3
  // 创建缓冲区对象
  var vertexBuffer = gl.createBuffer()
  // 将缓冲区对象绑定到目标
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
  // 向缓冲区对象中写入数据
  gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW)
  // 在顶点着色器中获取 a_Position 的地址;gl.program 为包含顶点着色器和片元着色器的着色器程序对象
  var a_Position = gl.getAttribLocation(gl.program, 'a_Position')
  // 将缓冲区对象分配给 a_Position 变量
  gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0)
  // 连接 a_Position 变量与分配给他的缓冲区对象
  gl.enableVertexAttribArray(a_Position)
  return n
}

// 将顶点的位置写入顶点着色器
var n = initVertexBuffers(gl)
// 指定清空 canvas 的颜色
gl.clearColor(0, 0, 0, 1)

var u_ModelMatrix = gl.getUniformLocation(gl.program, 'u_ModelMatrix')
var modelMatrix = new Matrix4()

var u_ViewMatrix = gl.getUniformLocation(gl.program, 'u_ViewMatrix')
var viewMatrix = new Matrix4()
viewMatrix.lookAt(100, 100, 100, 0, 0, 0, 0, 1, 0)

var u_ProjectionMatrix = gl.getUniformLocation(gl.program, 'u_ProjectionMatrix')
var projectionMatrix = new Matrix4()
// projectionMatrix.perspective(120, 1, 0.1, 1000)
projectionMatrix.ortho(-1, 1, -1, 1, 0.1, 1000)

function animate () {
  var now = Date.now()
  var duration = now - g_last
  g_last = now
  currentAngle = currentAngle + duration / 1000 * 180
}

function draw () {
  // clear canvas and add background color
  modelMatrix.setRotate(currentAngle, 0, 1, 0)
  gl.uniformMatrix4fv(u_ModelMatrix, false, modelMatrix.elements)
  gl.uniformMatrix4fv(u_ViewMatrix, false, viewMatrix.elements)
  gl.uniformMatrix4fv(u_ProjectionMatrix, false, projectionMatrix.elements)
  // 清空 canvas,清空区域,实际上是在清空颜色缓冲区(color buffer)
  gl.clear(gl.COLOR_BUFFER_BIT)
  // 第一个参数为绘制的类型,第二个参数是指定从哪个顶点开始绘制(整数型),第三个参数是指定绘制需要用到多少个顶点(整数型)
  // 当程序调用 l.drawArrays 方法时,顶点着色器将被执行 n 次,每次处理一个顶点。执行顶点着色器代码中的 main 函数。
  // 一旦顶点着色器执行完之后,片元着色器开始执行。
  gl.drawArrays(gl.TRIANGLES, 0, n)
}

tick()

渲染流程如下:

avatar

使用缓冲区对象向顶点着色器传入多个顶点的数据,需要遵循下面五个步骤:

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

对应代码:

function initVertexBuffers (gl) {
  // 定义顶点
  var vertices = new Float32Array([
    0, 0.5, -0.5, -0.5, 0.5, -0.5
  ])
  // 点的个数
  var n = 3
  // 创建缓冲区对象
  var vertexBuffer = gl.createBuffer()
  // 将缓冲区对象绑定到目标
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
  // 向缓冲区对象中写入数据
  gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW)
  // 在顶点着色器中获取 a_Position 的地址;gl.program 为包含顶点着色器和片元着色器的着色器程序对象
  var a_Position = gl.getAttribLocation(gl.program, 'a_Position')
  // 将缓冲区对象分配给 a_Position 变量
  gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0)
  // 连接 a_Position 变量与分配给他的缓冲区对象
  gl.enableVertexAttribArray(a_Position)
  return n
}
  • 创建缓冲区对象:gl.createBuffer() 是创建缓冲区对象;gl.deleteBuffer() 函数用来删除 gl.createBuffer() 创建出来的缓冲区对象。
  • 绑定缓冲区:将缓冲区对象绑定到 webgl 系统中已经存在的目标上。这个目标表示缓冲区对象的用途(在这里,就是向顶点着色器提供传给 attribute 变量的数据),这样 webgl 才能够正确处理其中的内容;

avatar

  • 缓冲区中写入数据:gl.bufferData(),该方法时将第二个参数中的数据写入到第一个参数 gl.ARRAY.BUFFER 上的缓冲区对象。我们不能直接向缓冲区写入数据,而只能向目标写入数据,所以要向缓冲区写数据,必须要先绑定; avatar

  • 使用类型化数组 Float32Array:为了绘制三维图形,webgl 通常需要同时处理大量相同类型的数据,例如顶点的坐标和颜色数据。为了优化性能,webgl 为每种基本数据类型引入了一种特殊的数组(类型化数组)。浏览器事先知道数组中的数据类型,所以处理起来也更加有效率。Float32Array 与普通数组不同,类数组不支持 push() 和 pop() 方法。注意创建类型化数组的唯一方法就是使用 new 操作符。不能使用 [] 运算符,那样创建的就是普通数组。 下面是类型化数组的方法、属性以及常量: avatar

  • 将缓冲区对象分配给 attribute 变量:gl.vertexAttributPointer(),之前可以使用 gl.vertexAttrib[1, 2, 3, 4]f 系列函数为 attribute 变量分配至,但是这些方法一次只能向 attribute 变量分配一个值,gl.vertexAttributPointer() 可以将整个数组一次性分配给 attribute 变量。

avatar

  • 开启 attribute 变量:gl.enableVertexAttribArray(),为了使顶点着色器能够访问缓冲区内的数据,需要使用 gl.enableVertexAttribArray() 开启 attribute 变量。同样可以使用 gl.disableVertexAttribArray() 来关闭分配。开启了 attribute 变量后,就不能使用 gl.vertexAttrib[1, 2, 3, 4]f 向他传递数据了,除非显式地关闭该 attribute 变量。

  • gl.drawArrays():

avatar

webgl 方法通过 gl.drawArrays() 的第一个参数指定不同的值,绘制不同的图形。下表的 v0,v1 等表示缓冲区 中的顶点,顶点的顺序将影响绘制的结果: avatar avatar

下面就是绘制这些基本的图形:

avatar

  • gl_PointSize 顶点着色器中设置点的大小,只有在绘制单个点的时候才起作用。

# 移动、旋转、缩放

# 移动

只需要传入坐标偏移量即可: u_Translation 就是传入的偏移量,每次 a_Position 加上偏移量之后,就会移动图形

var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'uniform vec4 u_Translation;\n' +
  'void main() {\n' +
  '  gl_Position = a_Position + u_Translation;\n' +
  '}\n';

// Fragment shader program
var FSHADER_SOURCE =
  'void main() {\n' +
  '  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n' +
  '}\n';

function main() {
  // Retrieve <canvas> element
  var canvas = document.getElementById('webgl');

  var 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 intialize shaders.');
    return;
  }

  var n = initVertexBuffers(gl);
  if (n < 0) {
    console.log('Failed to set the positions of the vertices');
    return;
  }

  // 偏移量传值
  var u_Translation = gl.getUniformLocation(gl.program, 'u_Translation');
  if (!u_Translation) {
    console.log('Failed to get the storage location of u_Translation');
    return;
  }
  gl.uniform4f(u_Translation, Tx, Ty, Tz, 0.0);

  gl.clearColor(0, 0, 0, 1);
  gl.clear(gl.COLOR_BUFFER_BIT);
  gl.drawArrays(gl.TRIANGLES, 0, n);
}

# 旋转

旋转必须要指明:

  • 旋转轴:图形将围绕旋转轴旋转
  • 旋转方向:方向是顺时针还是逆时针
  • 旋转角度:图形旋转经过的角度

描述旋转:绕 Z 轴旋转了 β 角度,如果 β 是正值,那么看到的物体就是逆时针旋转的。这种情况又称为正旋转。正旋转又称为右手法则旋转。

avatar

设点 p(x, y, z) 旋转 β 角度后变成 p'(x', y', z'):首先旋转是绕 Z 轴信息的,所以 z 坐标不会变化,可以直接忽略;然后 x 坐标和 y 坐标如下: avatar

上图,r 是从圆点到点 p 的距离,而 α 是 X 轴旋转到 p 的角度,用这两个变量计算出点 p 的坐标,如下:

  • x = r cos α
  • y = r sin α

类似地,可以使用 α 、r、β 来表示 p' 的坐标:

  • x' = r cos(α + β)
  • y' = r sin(α + β)

利用三角函数两角和公示,可得:

  • x' = r (cos α cos β - sin α sin β))
  • y' = r (sin α cos β + cos α sin β))

消除 r 和 α 后:

  • x' = x cosβ - y sinβ
  • y' = x sinβ - y cosβ
  • z' = z

我们可以将 sin β 和 cos β 的值传给顶点着色器,然后在着色器中根据上面的式子计算旋转后的点坐标,就可以实现旋转了。使用 JavaScript 内置的 Math 对象的 sin() 和 cos() 方法来进行三角函数运算。

代码如下:

var VSHADER_SOURCE =
  // x' = x cosβ - y sinβ
  // y' = x sinβ + y cosβ Equation 3.3
  // z' = z
  'attribute vec4 a_Position;\n' +
  'uniform float u_CosB, u_SinB;\n' +
  'void main() {\n' +
  '  gl_Position.x = a_Position.x * u_CosB - a_Position.y * u_SinB;\n' +
  '  gl_Position.y = a_Position.x * u_SinB + a_Position.y * u_CosB;\n' +
  '  gl_Position.z = a_Position.z;\n' +
  '  gl_Position.w = 1.0;\n' +
  '}\n';
var FSHADER_SOURCE =
  'void main() {\n' +
  '  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n' +
  '}\n';
  // 旋转角度
var ANGLE = 90.0; 

function main() {
  var canvas = document.getElementById('webgl');
  var 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 intialize shaders.');
    return;
  }
  var n = initVertexBuffers(gl);
  if (n < 0) {
    console.log('Failed to set the positions of the vertices');
    return;
  }

  // 将旋转图形所需的数据传递给顶点着色器
  var radian = Math.PI * ANGLE / 180.0; // 转换为弧度制
  var cosB = Math.cos(radian);
  var sinB = Math.sin(radian);

  var u_CosB = gl.getUniformLocation(gl.program, 'u_CosB');
  var u_SinB = gl.getUniformLocation(gl.program, 'u_SinB');
  if (!u_CosB || !u_SinB) {
    console.log('Failed to get the storage location of u_CosB or u_SinB');
    return;
  }
  gl.uniform1f(u_CosB, cosB);
  gl.uniform1f(u_SinB, sinB);
  gl.clearColor(0, 0, 0, 1);
  gl.clear(gl.COLOR_BUFFER_BIT);
  gl.drawArrays(gl.TRIANGLES, 0, n);
}

function initVertexBuffers(gl) {
  var vertices = new Float32Array([
    0, 0.5,   -0.5, -0.5,   0.5, -0.5
  ]);
  var n = 3;
  var vertexBuffer = gl.createBuffer();
  if (!vertexBuffer) {
    console.log('Failed to create the buffer object');
    return -1;
  }
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

  var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
  if (a_Position < 0) {
    console.log('Failed to get the storage location of a_Position');
    return -1;
  }
  gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
  gl.enableVertexAttribArray(a_Position);

  return n;
}

# 变换矩阵旋转

我们可以将上面的操作看成是矩阵计算得到的结果:

avatar 上面的这个矩阵通常称为旋转矩阵。

# 变换矩阵平移

平移时需要加上一个常量,来表示偏移量,可以使用 4 × 4 的矩阵以及具有第四个分量的矢量来表示,假设 p 的坐标为(x, y, z, 1),平移之后的 p' 的坐标为(x', y', z', 1);如下: avatar

该矩阵的乘法结果如下:

  • x' = ax + by + cz + d
  • y' = ex + fy + gz + h
  • z' = ix + jy + kz + l
  • 1 = mx + ny + oz + p

根据最后一个式子就可以计算出系数 m = 0, n = 0, o = 0, p = 1, 这些方式都有常数项 d、h、l 和 p,我们跟之前做平移的等式进行比较:

  • x' = x + Tx
  • y' = y + Ty
  • z' = z + Tz

比较两个等式,姐可以知道 a = 1,b = 0,c = 0, d = Tx,e = 0,f = 1,g = 0,h = Ty,i = 0,j = 0,k = 1,l = Tz。这样就可以表示平移矩阵,如下:

avatar

# 使用变换矩阵实现旋转平移

如果我们使用最开始计算点坐标在进行旋转平移变化,每次都需要进行一次新的变换。每次重新取一个新的等式,然后实现一个新的着色器,很不科学,我们可以通过变换矩阵来完成。 上面我们获取到旋转的矩阵为 3 × 3 的矩形,但是平移矩阵是 4 × 4 的矩阵,阶数不同不能进行计算,可以处理一下旋转矩阵为 4 × 4 的。 avatar

实现代码如下:

var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  // mat4 表示 4 × 4 的矩阵
  'uniform mat4 u_xformMatrix;\n' +
  'void main() {\n' +
  // 矩阵相乘
  '  gl_Position = u_xformMatrix * a_Position;\n' +
  '}\n';

var FSHADER_SOURCE =
  'void main() {\n' +
  '  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n' +
  '}\n';
var ANGLE = 90.0;

function main() {
  var canvas = document.getElementById('webgl');
  var 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 intialize shaders.');
    return;
  }
  var n = initVertexBuffers(gl);
  if (n < 0) {
    console.log('Failed to set the positions of the vertices');
    return;
  }
  // 创建旋转矩阵
  var radian = Math.PI * ANGLE / 180.0; // 角度转换为弧度制
  var cosB = Math.cos(radian), sinB = Math.sin(radian);
  // 注意 webgl 中矩阵是列主序的
  var xformMatrix = new Float32Array([
     cosB, sinB, 0.0, 0.0,
    -sinB, cosB, 0.0, 0.0,
      0.0,  0.0, 1.0, 0.0,
      0.0,  0.0, 0.0, 1.0
  ]);
  // 将旋转矩阵传输给顶点着色器
  var u_xformMatrix = gl.getUniformLocation(gl.program, 'u_xformMatrix');
  if (!u_xformMatrix) {
    console.log('Failed to get the storage location of u_xformMatrix');
    return;
  }
  // 将矩阵传递给着色器 v 表示可以向着色器传输多个数据值
  // 第一个参数为 uniform 变量的存储位置,第二个参数在 webgl 中必须指定 false,第三个参数是待传输的类型化数组,4 × 4 矩阵按列主序存储在其中 
  gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix);
  gl.clearColor(0, 0, 0, 1);
  gl.clear(gl.COLOR_BUFFER_BIT);
  gl.drawArrays(gl.TRIANGLES, 0, n);
}

function initVertexBuffers(gl) {
  var vertices = new Float32Array([
    0, 0.5,   -0.5, -0.5,   0.5, -0.5
  ]);
  var n = 3;
  var vertexBuffer = gl.createBuffer();
  if (!vertexBuffer) {
    console.log('Failed to create the buffer object');
    return false;
  }

  gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

  var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
  if (a_Position < 0) {
    console.log('Failed to get the storage location of a_Position');
    return -1;
  }
  gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
  gl.enableVertexAttribArray(a_Position);
  return n;
}

注意这里的矩阵表示法,数组为一维的,其元素排成一行,我们可以按照两种方式在数组中存储矩阵元素:按行主序和按列主序,如图:

avatar

webgl 跟 opengl 一样,矩阵元素是按列主组存储在数组中的。

# 变换矩阵 - 缩放

假设 p 经过缩放操作变成了 p':

avatar

假设在三个方向 X 轴,Y 轴,Z 轴的缩放因子 Sx,Sy,Sz 不相关,那么有:

  • x' = Sx × x
  • y' = Sy × y
  • z' = Sz × z

写成变换矩阵如下:

avatar

我们只需要修改前面代码的矩阵就可以实现缩放效果。

评 论:

更新: 3/4/2021, 1:00:21 AM