前言
主要参考webgl编程指南和webglfundamentals进行学习。本文主要对学习的内容进行总结,理清基本概念和工作原理。
基础概念
WebGL只关心两件事:裁剪空间中的坐标值和颜色值。使用WebGL只需要给它提供这两个东西。
https://webglfundamentals.org/webgl/lessons/zh_cn/webgl-shaders-and-glsl.html
顶点着色器(vertex-shader)
每个顶点调用一次(顶点)着色器,每次调用都需要设置一个特殊的全局变量gl_Position, 该变量的值就是裁减空间坐标值。
顶点着色器需要的数据,可以通过以下三种方式获得。
- Attributes 属性 (从缓冲中获取的数据,可由js获取并设置)
- Uniforms 全局变量 (在一次绘制中对所有顶点保持一致值)
- Textures 纹理 (从像素或纹理元素中获取的数据)
// 一个属性值,将会从缓冲中获取数据
attribute vec4 a_position;
// 所有着色器都有一个main方法
void main() {
// gl_Position 是一个顶点着色器主要设置的变量
gl_Position = a_position;
}
在JavaScript中你可以把它想象成 a_position = {x: 0, y: 0, z: 0, w: 0}。 z和w还是默认值 0 和 1 。
无论你的画布有多大,裁剪空间的坐标范围永远是 -1 到 1 。
片断着色器(fragment-shader)
每个像素都将调用一次片断着色器,每次调用需要从你设置的特殊全局变量gl_FragColor中获取颜色信息。
片断着色器所需的数据,可以通过以下三种方式获取
- Uniforms 全局变量 (全局变量在一次绘制过程中传递给着色器的值都一样在一次绘制过程中传递给着色器的值都一样)
- Textures 纹理
- Varyings 可变量 (可变量是一种顶点着色器给片断着色器传值的方式)
// 片断着色器没有默认精度,所以我们需要设置一个精度
// mediump是一个不错的默认值,代表“medium precision”(中等精度)
precision mediump float;
void main() {
// gl_FragColor是一个片断着色器主要设置的变量
gl_FragColor = vec4(1, 0, 0.5, 1); // 返回“红紫色”
}
gl_FragColor是vec4类型,包括4个浮点分向量,分别代表RGBA值。
颜色空间 空间坐标(0 -> 1),和顶点顶点着色器的剪裁空间坐标范围不同。
工作原理
WebGL在GPU上的工作基本上分为两部分,第一部分是将顶点(或数据流)转换到裁剪空间坐标, 第二部分是基于第一部分的结果绘制像素点。
左侧是你提供的数据。顶点着色器(Vertex Shader)每个顶点调用一次,在这个方法中做一些数学运算后设置了一个特殊的gl_Position变量, 这个变量就是该顶点转换到裁剪空间中的坐标值,GPU接收该值并将其保存起来。
假设你正在画三角形,顶点着色器每完成三次顶点处理,WebGL就会用这三个顶点画一个三角形。 它计算出这三个顶点对应的像素后,就会光栅化这个三角形,“光栅化”其实就是“用像素画出来” 的花哨叫法。对于每一个像素,它会调用你的片断着色器询问你使用什么颜色。 你通过给片断着色器的一个特殊变量gl_FragColor设置一个颜色值,实现自定义像素颜色。
想要从顶点着色器传值到片断着色器,我们可以定义“可变量(varyings)”。
可以简单的说,webgl每画一个像素点,由顶点着色器提供坐标,片段着色器提供颜色
开始绘制
var canvas = document.getElementById("canvas");
canvas.width = 400;
canvas.height = 300;
var gl = canvas.getContext("webgl");
// 裁剪空间的 -1 -> +1 分别对应到x轴的 0 -> gl.canvas.width 和y轴的 0 -> gl.canvas.height。
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
// 创建着色器方法,输入参数:渲染上下文,着色器类型,数据源
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);
}
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 vertexShaderSource = `
attribute vec4 a_position;
void main() {
gl_Position = a_position;
}
`;
var fragmentShaderSource = `
precision mediump float;
void main() {
gl_FragColor = vec4(1, 0, 0.5, 1); // 返回“瑞迪施紫色”
}
`;
var vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
var fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
var program = createProgram(gl, vertexShader, fragmentShader);
var positionLocation = gl.getAttribLocation(program, "a_position");
gl.useProgram(program);
function drawTriangles() {
var positionBuffer = gl.createBuffer(); //创建缓冲区
// 使用缓冲,将缓冲区绑定到目标
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
// 三个二维点坐标
var positions = [
0, 0,
0, 0.5,
0.7, 0,
0, 1,
];
// 从positions中复制数据到序列中,然后gl.bufferData复制这些数据到GPU的positionBuffer对象
// 向缓冲区写入数据
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
// 连接a_position对象与分配给它的缓冲区对象
gl.enableVertexAttribArray(positionLocation);
var size = 2; // 每次迭代运行提取两个单位数据
var type = gl.FLOAT; // 每个单位的数据类型是32位浮点型
var normalize = false; // 不需要归一化数据
var stride = 0; // 0 = 移动单位数量 * 每个单位占用内存(sizeof(type))
// 每次迭代运行运动多少内存到下一个数据开始点
var offset = 0; // 从缓冲起始位置开始读取
//将缓冲区对象分配给a_position变量
gl.vertexAttribPointer(positionLocation, size, type, normalize, stride, offset);
var primitiveType = gl.TRIANGLES;
var offset = 1;
var count = 3;
gl.drawArrays(primitiveType, offset, count);
setTimeout(() => {
positions = [
0, 0,
1, 0.5,
0.7, 0,
0, 1,
];
// 重新写入数据
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
// 重新绘制
gl.drawArrays(primitiveType, offset, count);
}, 10000)
}
drawTriangles();
总结
webgl提供一种机制,缓存区对象(buffer object),它可以一次性地向着色器传入多个顶点数据。缓存区对象是webgl系统中的一块内存区域,可以保存大量顶点数据,供着色器使用。
写入顶点坐标
gl.getAttribLocation用来获取attributes变量
webgl通常需要处理大量数据。为了优化性能,webgl引入了一种特殊的数组(类型化数组)浏览器事先知道数组类型,处理起来也更加有效率。Float32Array就是一种类型化数组。应当牢记,webgl很多操作都需要用到类型化数组。
- 创建缓冲区对象(gl.createBuffer)
- 绑定缓冲区对象(gl.bindBuffer)
- 将数据写入缓冲区对象(gl.bufferData)
- 将缓冲区分配给一个attributes变量(gl.vertexAttribPointer)
- 开启attributes变量(gl.enableVertexAttribArray)
图源光栅化
发生在顶点着色器和片元着色器之间的从图形到片元的转换,又称为图元光栅化。“光栅化”其实就是“用像素画出来” 的花哨叫法。对于每一个像素,它会调用你的片断着色器询问你使用什么颜色。
- 图形装配过程 这一步的任务是将鼓励的顶点坐标装配成几何图形。几何图形的类别由gl.drawArray() 函数单第一个参数决定。
- 光栅化 将装配好的几何图形转化为片元。
程序会将三个点装配成首尾相连的折线段,并光栅化一个空心的三角形(不产生中间的像素)
调用片元着色器
一旦光栅化过程结束后,程序就开始逐片元调用片元着色器。
纹理
纹理:将一张真实世界的图片贴到一个由两个三角形组成的矩形上,这样的矩形表面上看就是这张图片,此时这张图片又可称为纹理。
纹理映射:为光栅化后的每个片元图上合适的颜色