WebGL入门
画点
首先我们先写一个最简单的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>Colored Point</title>
</head>
<body onload="main()">
<canvas id="webgl" width="400" height="400">
Please use the browser supporting "canvas"
</canvas>
<script src="../lib/webgl-utils.js"></script>
<script src="../lib/webgl-debug.js"></script>
<script src="../lib/cuon-utils.js"></script>
<script src="ColoredPoints.js"></script>
</body>
</html>
我们可以看到,除了最底下的我们自定义的js文件,还引入了三个工具函数文件。里面是一些封装好的方法(如果你也想下载,参考这里),目前我们只需关注cuon-utils.js里面的东西,里面是对一些渲染流程步骤的封装,待会讲解。
// ColoredPoints.js
// 顶点着色器
let VSHADER_SOURCE = /* glsl */ `
attribute vec4 a_Position;
void main() {
gl_Position = a_Position;
gl_PointSize = 10.0;
}
`
// 片元着色器
let FSHADER_SOURCE = /* glsl */`
precision mediump float; // 精度限制
uniform vec4 u_FragColor;
void main() {
gl_FragColor = u_FragColor;
}
`
function main() {
const canvas = document.getElementById('webgl');
const gl = getWebGLContext(canvas); // 获取webgl上下文
if (!gl) {
console.log('Failed to get the rendering context for WebGL');
return;
}
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) { // 创建shader
console.log('Failed to initialize shaders.');
return;
}
// 向顶点着色器中的变量传值
let a_Position = gl.getAttribLocation(gl.program, 'a_Position');
if (a_Position < 0) {
console.log('Failed to get the storage location of a_Position');
return;
}
// 向片元着色器中的变量传值
let u_FragColor = gl.getUniformLocation(gl.program, 'u_FragColor');
if (!u_FragColor) {
console.log('Failed to get u_FragColor variable');
return;
}
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;
let y = ev.clientY;
let rect = ev.target.getBoundingClientRect();
// 将点的坐标从浏览器坐标转换成webgl坐标
x = ((x - rect.left) - canvas.width / 2) / (canvas.width / 2);
y = ((canvas.height / 2) - (y - rect.top)) / (canvas.height / 2);
g_points.push([x, y]);
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, 1.0, 0.0, 1.0]);
} else {
g_colors.push([1.0, 1.0, 1.0, 1.0]);
}
gl.clear(gl.COLOR_BUFFER_BIT);
var len = g_points.length;
for(var i = 0; i < len; i++) {
let xy = g_points[i];
let rgba = g_colors[i];
gl.vertexAttrib3f(a_Position, xy[0], xy[1], 0.0);//给shader中的变量赋值
gl.uniform4f(u_FragColor, rgba[0], rgba[1], rgba[2], rgba[3]);//给shader中的变量赋值
gl.drawArrays(gl.POINTS, 0, 1);// 执行绘制的方法
}
}
现在你已经大致看过了上述代码,我们来一起走一遍流程。
首先是执行body标签中onload的main()方法。然后我们会获取webgl的上下文,getWebGLConetext()是cuon-utils.js中封装的方法,之后我们会创建着色器,和webgl上下文绑定。接着我们需要调用apigetAttribLocation()或getUniformLocation()拿到在shader中设置的变量,为之后的点击事件做准备。然后是正常流程中的clearColor()设置背景颜色和clear()清除之前的绘制,绘制新画面。另外,我们给canvas绑定了一个点击事件,在点击时,拿到我们当前点击的坐标,然后转换成webgl坐标,经过自定义的颜色逻辑,把坐标数据和颜色数据传入之前获取到的变量里,最后调用drawArrays()来绘制出点击的点。
接下来,我们进入到cuon-utils.js中分析我们用到的两个方法getWebGLContentx()和initShaders()。
// getWebGLContext()分析
// 我们在cuton-utils.js中找到getWebGLContext,然后经过重重方法的调用,找到了最本质的逻辑
var create3DContext = function(canvas, opt_attribs) {
var names = ["webgl", "experimental-webgl", "webkit-3d", "moz-webgl"];
var context = null;
for (var ii = 0; ii < names.length; ++ii) {
try {
context = canvas.getContext(names[ii], opt_attribs);
} catch(e) {}
if (context) {
break;
}
}
return context;
}
很明显,主要就是在创建webgl上下文时,做浏览器的兼容。
// initShaders()分析
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;
}
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 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;
}
我们来分析一下在webgl中如何创建shader。主要步骤如下:
- 创建webgl上下文
gl.createShader(type)创建一个shadergl.shaderSource(shader, source)绑定我们写好的shader代码,source即是我们写好的代码,在javascript中shader代码以字符串形式编写。gl.compileShader(shader)编译执行shadergl.createProgram()创建program,整个着色器的渲染都与这个program绑定gl.attachShader(program, shader)把前面步骤创建好的shader绑定到program上gl.linkProgram(program)绑定这个program到webgl上下文gl.useProgram(program)启动这个program
总结一下,目前绘图的流程如下: 获取canvas元素 -> 获取WebGL绘图上下文 -> 初始化着色器 -> 设置canvas背景色 -> 清除之前的绘图内容 -> 绘图。
api
在上述例子中,还涉及到一些webgl的api,简单介绍
clearColor()
gl.clearColor()用来设置canvas的背景色,不知道有没有和我一样被方法名迷惑了,简单把它当做是setBackgroundColor即可。
gl.clearColor(red, green, blue, alpha)
clear()
gl.clear() // 用设置好的背景色填充绘图区域,擦除之前已经绘制的内容,接收三个参数分别是:
gl.COLOR_BUFFER_BIT 颜色缓冲区
gl.DEPTH_BUFFER_BIT 深度缓冲区
gl.STENCIL_BUFFER_BIT 模板缓冲区
drawArrays()
gl.drawArrays(mode, first, count)
// mode表示指定的方式,可接收如下常量:gl.POINTS, gl.LINES, gl.LINE_STRIP, gl.LINE_LOOP, gl.TRIANGLES, gl.TRIANGLE_STRIP, gl.TRIANGLE_FAN
// first 表示从那个顶点开始绘制
// count标志绘制需要多少个顶点
shader编写
如果你是在vscode中编写shader的代码,推荐装插件Comment tagged templates。然后我们在编写的字符串的前面添加注释,编辑器便能按照对应语言高亮。
例如:
let VSHADER_SOURCE = /* glsl */ `
attribute vec4 a_Position;
void main() {
gl_Position = a_Position;
gl_PointSize = 10.0;
}
`
shader中文为着色器,分为顶点着色器(Vertex shader)和片元着色器(Fragment shader)。顶点着色器用来处理每个顶点,计算出每个顶点的位置,片元着色器用来对光栅化后的像素进行渲染。
齐次坐标
在设置顶点的坐标时,我们例子中是gl.vertexAttrib3f(a_Position, xy[0], xy[1], 0.0)。因为我们目前只演示二维平面,所以最后z轴设置成了0.0。其实,顶点位置的设置有四个参数,里面涉及到了齐次坐标相关的知识。齐次坐标使用如下的符号描述:(x,y,z,w)。齐次坐标(x,y,z,w)等价于三维坐标(x/w,y/w,z/w)。所以如果齐次坐标的第四个分量是1,我们可以将它当做三维坐标使用。
WebGL中的坐标系
- canvas的中心点:(0.0, 0.0, 0.0)
- canvas的左边缘和右边缘:(-1.0, 0.0, 0.0) 和 (1.0, 0.0, 0.0)
- canvas的上边缘和下边缘:(0.0, -1.0, 0.0) 和 (0.0, 1.0, 0.0)
在上面的代码中,我们有一段逻辑是将浏览器的点击坐标转成webgl的坐标,我们进行分析一下。
x = ((x - rect.left) - canvas.width / 2) / (canvas.width / 2);
y = ((canvas.height / 2) - (y - rect.top)) / (canvas.height / 2);
首先x是clientX,是点击的点在浏览器中的x坐标,rect.left是canvas左上顶点距离浏览器的左边距离。
(x - rect.left)即是点击的点在canvas中的坐标。canvas.width / 2和canvas.height / 2是canvas的中心点的坐标。
((x - rect.left) - canvas.width / 2) 相当于当前的点相对于中心点来说它的坐标是多少,也就是我们把原本canvas左上角的原点转移到了canvas的中心。
之后除以(canvas.width / 2)相当于把相对的像素值转成0-1的范围,因为webgl的坐标值是[-1.0, 1.0]的。
举个例子,如果我们有一个canvas,长200px,宽200px。我们先假设canvas的左上角即是浏览器视窗的左上角,省去rect.left和rect.top的逻辑。假如我们刚好点击一个点,这个点的坐标是[50,50]。然后我们开始转换逻辑。canvas的中心点是[100, 100]。此时点击的点相对canvas来说是[50 - 100, 100 - 50]变成[-50, 50],然后转成webgl坐标变成[-50 / 100, 50 / 100]即是[-0.5, 0.5]。
如何在JavaScript和着色器之间传输数据?
attribute变量,uniform变量。attribute变量传输的是那些与顶点相关的数据,而uniform变量传输的是那些对于所有顶点都相同(或与顶点无关)的数据。比如每个顶点的位置是不一样的,我们用attribute传值,而有时我们需要对这个图像做变换,类似对整个图像进行平移,每个顶点都是平移相同的距离,那我们就用uniform变量。
使用辅助函数initShaders()在WebGL系统中建立了顶点着色器。然后,WebGL会对着色器进行解析,辨识出着色器具有的attribute变量。每个变量都具有一个存储地址,以便通过存储地址向变量传输数据。
gl.getAttribLocation(program, name)
// program 指定包含顶点着色器和片元着色器的着色器程序对象
// name 指定想要获取其存储地址的attribute变量的名称
gl.vertextAttrib3f(location, v0, v1, v2)
// location 指定将要修改的attribute变量的存储位置
// v0, v1, v2 对应坐标值
除了vertextAttrib3f外,还有
vertextAttrib1f(location, v0)vertextAttrib2f(location, v0, v1)vertextAttrib4f(location, v0, v1, v2, v3)
OpenGL ES 2.0命名规范
gl.{基础函数名}{参数个数}{参数类型}
同样,gl.uniform4f也有uniform1f,uniform2f,uniform3f。