# 你好世界

  • createShader shaderSource compileShader
  • getShaderParameter getShaderInfoLog deleteShader
  • createProgram attachShader linkProgram
  • getProgramParameter getShaderInfoLog deleteProgram
  • getAttribLocation
  • createBuffer bindBuffer bufferData
  • viewport clearColor clear
  • useProgram
  • enableVertexAttribArray vertexAttribPointer
  • drawArrays

GLSL

  • attribute
  • vec4
  • gl_Position
  • precision highp mediump lowp
  • gl_FragColor

# 编写着色器

让我们从顶点着色器开始

Demo1VertexShaderSource.glsl

// 一个属性值,将会从缓冲中获取数据
attribute vec4 a_position;

// 所有着色器都有一个main方法
void main() {

  // gl_Position 是一个顶点着色器主要设置的变量
  gl_Position = a_position;
}

接下来我们需要一个片断着色器

Demo1FragmentShaderSource.glsl

// 片断着色器没有默认精度,所以我们需要设置一个精度
// mediump 是一个不错的默认值,代表“medium precision”(中等精度)
precision mediump float;

void main() {
  // gl_FragColor是一个片断着色器主要设置的变量
  gl_FragColor = vec4(1, 0, 0.5, 1); // 返回“红紫色”
}

上方我们设置 gl_FragColor 为 1, 0, 0.5, 1,其中 1 代表红色值,0 代表绿色值, 0.5 代表蓝色值,最后一个 1 表示阿尔法通道值。 WebGL 中的颜色值范围从 0 到 1 。

# 渲染流程

# 获取上下文

首先创建一个 WebGL 渲染上下文

<canvas ref="canvas" id="webgl" width="320" height="240"></canvas>

这里我使用了 Vue 的 ref 来获取对 canvas 标签的引用,你也可以使用 document 的元素获取方法

const canvasEl = this.$refs.canvas;
const gl = this.canvasEl.getContext("webgl");

# 编译着色器

现在我们需要编译着色器对然后提交到 GPU,这里着色器源码的引入我使用了webpackraw-loader
你也可以使用 DOM 对象的 text 属性来获取 script 标签中的 GLSL 源码

import vertexShaderSource from "./Demo1VertexShaderSource.glsl";
import fragmentShaderSource from "./Demo1FragShaderSource.glsl";
// 创建着色器方法,输入参数:渲染上下文,着色器类型,数据源
function createShaderFromSource(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);
}

现在我们可以创建真正的着色器方法了

const vertexShader = createShaderFromSource(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = createShaderFromSource(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);

然后我们将这两个着色器 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);
}

然后调用它

const program = createProgram(gl, vertexShader, fragmentShader);

# 获取绑定点和创建缓冲

现在我们已经在 GPU 上创建了一个 GLSL 着色程序,我们还需要给它提供数据。 WebGL 的主要任务就是设置好状态并为 GLSL 着色程序提供数据。

在这个例子中 GLSL 着色程序的唯一输入是一个属性值 a_position。 我们要做的第一件事就是从刚才创建的 GLSL 着色程序中找到这个属性值所在的位置。

找到哪个我们在 GLSL 中定义的属性a_position

const positionAttributeLocation = gl.getAttribLocation(program, "a_position");

GLSL 的属性值从 JS 缓冲中获取数据,所以我们创建一个缓冲

const positionBuffer = gl.createBuffer();

绑定点

WebGL 可以通过绑定点操控全局范围内的许多数据,你可以把绑定点想象成一个 WebGL 内部的全局变量

现在让我们来绑定位置信息缓冲,就绑定到gl.ARRAY_BUFFER吧。

gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);

然后们需要通过绑定点向缓冲中存放数据

// 通过绑定点向缓冲中存放数据
gl.bufferData(
  gl.ARRAY_BUFFER,
  new Float32Array([-0.5, -0.25, 0, 0.5, 0.5, -0.25]), // 一个等腰三角形
  gl.STATIC_DRAW
);

这里完成了一系列事情,第一件事是我们有了一个 JavaScript 序列 positions。 然而 WebGL 需要强类型数据,所以 new Float32Array(positions)创建了 32 位浮点型数据序列, 并从 positions 中复制数据到序列中,然后 gl.bufferData 复制这些数据到 GPU 的 positionBuffer 对象上。

它最终传递到 positionBuffer 上是因为在前一步中我们我们将它绑定到了 ARRAY_BUFFER(也就是绑定点)上。

最后一个参数gl.STATIC_DRAW是提示 WebGL 我们将怎么使用这些数据。 WebGL 会根据提示做出一些优化。 gl.STATIC_DRAW 提示 WebGL 我们不会经常改变这些数据。

# 渲染前的准备

我们需要告诉 WebGL 怎样把提供的 gl_Position 裁剪空间坐标对应到画布像素坐标, 通常我们也把画布像素坐标叫做屏幕空间。 为了实现这个目的,我们只需要调用 gl.viewport 方法并传递画布的当前尺寸。

gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

我们用 0, 0, 0, 0 清空画布

// 清空画布
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);

我们需要告诉 WebGL 运行哪个着色程序

// 告诉它用我们之前写好的着色程序(一个着色器对)
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);

# 调用绘制方法

现在我们终于可以让 WebGL 运行我们的 GLSL 着色程序了。

var primitiveType = gl.TRIANGLES;
var offset = 0;
var count = 3;
gl.drawArrays(primitiveType, offset, count);

# 渲染结果

在上例中可以发现顶点着色器只是简单的传递了位置信息。 由于位置数据坐标就是裁剪空间中的坐标,所以顶点着色器没有做什么特别的事。

如果你想做三维渲染,你需要提供合适的着色器将三维坐标转换到裁剪空间坐标,因为WebGL只是一个光栅化API。

# 流程总结