通过对《webGL入门与实践》一书为期一周的学习,仿照书中案例,终于写出了一个基本的webGL程序,也算掌握了webGL的基本绘制过程…
本篇内容涉及一些术语,为了便于理解,以下是对涉及到的几个相关术语的简单解释:
- 图元:WebGL 能够绘制的基本图形元素,包含三种:点、线段、三角形。
- 片元:可以理解为像素,像素着色阶段是在片元着色器中。
- 裁剪坐标系:裁剪坐标系是顶点着色器中的 gl_Position 内置变量接收到的坐标所在的坐标系。
- 设备坐标系:又名 NDC 坐标系,是裁剪坐标系各个分量对 w 分量相除得到的坐标系,特点是 x、y、z 坐标分量的取值范围都在 [-1 , 1]之间,可以将它理解为边长为 2 的正方体,坐标系原点在正方体中心。
实现一个简单的 WebGL 程序:鼠标点击一次,就会在点击位置处绘制一个随机颜色的点。麻雀虽小,但五脏俱全,使用 WebGL 绘制一个点虽然简单,但是它仍需要 JavaScript 程序和着色器程序共同完成。
所以通过这个例子,能够基本掌握 WebGL 的绘制过程。
效果如下:
从着色器程序开始入手,先用GLSL编写顶点着色器和片元着色器。
void main(){
//声明顶点的位置
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
//声明待绘制点的大小。
gl_PointSize = 10.0;
}
void main(){
//设置像素的填充颜色为红色。
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
至此,我们完成了着色器的开发,是不是灰常 easy 呢?
HTML 文件至少需要包含一个 canvas 标签,另外需要两个存储着色器源码的 script 标签。
<body>
<script type="shader-source" id="vertexShader">
void main(){
//声明顶点位置
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
//声明要绘制的点的大小。
gl_PointSize = 10.0;
}
script>
<script type="shader-source" id="fragmentShader">
void main(){
//设置像素颜色为红色
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
script>
<canvas id="canvas">canvas>
body>
首先,获取 WebGL 绘图环境:
var canvas = document.querySelector('#canvas');
var gl = canvas.getContext('webgl') || canvas.getContext("experimental-webgl");
在某些浏览器中,我们还需要做下兼容处理,加上实验前缀。
创建顶点着色器对象:
// 获取顶点着色器源码
var vertexShaderSource = document.querySelector('#vertexShader').innerHTML;
// 创建顶点着色器对象
var vertexShader = gl.createShader(gl.VERTEX_SHADER);
// 将源码分配给顶点着色器对象
gl.shaderSource(vertexShader, vertexShaderSource);
// 编译顶点着色器程序
gl.compileShader(vertexShader);
接下来,创建片元着色器,该过程和顶点着色器的创建过程类似,区别在于着色器源码和着色器类型。
// 获取片元着色器源码
var fragmentShaderSource = document.querySelector('#fragmentShader').innerHTML;
// 创建片元着色器程序
var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
// 将源码分配给片元着色器对象
gl.shaderSource(fragmentShader, fragmentShaderSource);
// 编译片元着色器
gl.compileShader(fragmentShader);
着色器对象创建完毕,接下来开始创建着色器程序
//创建着色器程序
var program = gl.createProgram();
//将顶点着色器挂载在着色器程序上。
gl.attachShader(program, vertexShader);
//将片元着色器挂载在着色器程序上。
gl.attachShader(program, fragmentShader);
//链接着色器程序
gl.linkProgram(program);
有时候一个 WebGL 应用包含多个 program,所以在使用某个 program 绘制之前,我们要先启用它。
// 使用刚创建好的着色器程序。
gl.useProgram(program);
接下来开始绘制:
//设置清空画布颜色为黑色。
gl.clearColor(0.0, 0.0, 0.0, 1.0);
//用上一步设置的清空画布颜色清空画布。
gl.clear(gl.COLOR_BUFFER_BIT);
//绘制点。
gl.drawArrays(gl.POINTS, 0, 1);
gl.drawArrays 是执行绘制的 API,上面示例中的第一个参数 gl.POINTS 代表我们要绘制的是点图元,第二个参数代表要绘制的顶点的起始位置,第三个参数代表顶点绘制个数。
至此,着色器部分和 JavaScript 程序都写完了,运行看下效果:
上面这些代码实现了点的绘制。但是我们发现,在== gl.drawArrays== 方法之前有很多重复的代码,而且这些重复代码是我们几乎实现每个 WebGL 应用都要编写的,So,我们把这些代码封装一下,封装出的函数库放在 webgl-helper.js 文件中,优化过后的代码如下:
//首先获取canvas,这步就不用说了吧
var canvas = getCanvas(id);
//获取webgl绘图环境
var gl = getWebGLContext(canvas);
//顶点着色器创建
var vertexShader = createShaderFromScript(gl, gl.VERTEX_SHADER,'vertexShader');
//片元着色器创建
var fragmentShader = createShaderFromScript(gl, gl.FRAGMENT_SHADER,'fragmentShader');
//创建着色器程序
var program = createProgram(gl ,vertexShader, fragmentShader);
gl.useProgram(program);//告诉 WebGL 运行哪个着色器程序
gl.clearColor(0.0, 0.0, 0.0, 1.0);//设置清空画布颜色为黑色。
gl.clear(gl.COLOR_BUFFER_BIT);//用上一步设置的清空画布颜色清空画布。
gl.drawArrays(gl.POINTS, 0, 1);//绘制点
OK,看来比之前简洁了很多。
首先修改一下着色器程序,修改后的着色器程序要能够接收 JavaScript 传递过来的数据
//设置浮点数精度为 中等精度
precision mediump float;
//接收点在 canvas 坐标系上的坐标,即(x,y)
attribute vec2 a_Position;//attribue 变量只能在顶点着色器中定义。
//接收 canvas 的宽高
attribute vec2 a_Screen_Size;
void main(){
// start
//将屏幕坐标系转化为裁剪坐标(裁剪坐标系),将屏幕坐标系转化为裁剪坐标(裁剪坐标系)
vec2 position = (a_Position / a_Screen_Size) * 2.0 - 1.0;
position = position * vec2(1.0, -1.0);
gl_Position = vec4(position, 0, 1);
// end
//声明要绘制的点的大小。
gl_PointSize = 10.0;
}
//设置浮点数精度为中等精度
precision mediump float;
//接收 JavaScript 传过来的颜色值(RGBA)。
uniform vec4 u_Color; //uniform 变量既可以在顶点着色器中定义,也可以在片元着色器中定义。
void main(){
//将普通的颜色表示转化为 WebGL 需要的表示方式,即将【0-255】转化到【0,1】之间。
vec4 color = u_Color / vec4(255, 255, 255, 1);
gl_FragColor = color;
}
在这里从 Canvas 坐标系转变到 NDC 坐标系(即设备坐标系),这个变换比较简单,基本运算就可以实现。如果对webGL比较熟悉了,可以用一种更通用的转换方法:矩阵变换。
<script type="shader-source" id="vertexShader">
precision mediump float;
//接收点在 canvas 坐标系上的坐标 (x, y)
attribute vec2 a_Position;
//接收 canvas 窗口尺寸(width, height)
attribute vec2 a_Screen_Size;
void main(){
//将屏幕坐标系转化为 GLSL 限定的坐标值(NDC坐标系)
vec2 position = (a_Position / a_Screen_Size) * 2.0 - 1.0;
position = position * vec2(1.0, -1.0);
gl_Position = vec4(position, 0, 1);
//声明要绘制的点的大小。
gl_PointSize = 10.0;
}
script>
<script type="shader-source" id="fragmentShader">
precision mediump float;
//接收 JavaScript 传过来的颜色值(rgba)。
uniform vec4 u_Color;
void main(){
vec4 color = u_Color / vec4(255, 255, 255, 1);
gl_FragColor = color;
}
script>
<canvas id="canvas">canvas>
这里省略了着色器创建部分…
//找到顶点着色器中的变量
var a_Position = gl.getAttribLocation(program, 'a_Position');
//找到顶点着色器中的变量
var a_Screen_Size = gl.getAttribLocation(program, 'a_Screen_Size');
//找到片元着色器中的变量
var u_Color = gl.getUniformLocation(program, 'u_Color');
//为顶点着色器中的 a_Screen_Size 传递 canvas 的宽高信息
gl.vertexAttrib2f(a_Screen_Size, canvas.width, canvas.height);
//存储点击位置的数组。
var points = [];
canvas.addEventListener('click', e => {
var x = e.pageX;
var y = e.pageY;
var color = randomColor();
points.push({ x: x, y: y, color: color })
gl.clearColor(0, 0, 0, 1.0);
//用上一步设置的清空画布颜色清空画布。
gl.clear(gl.COLOR_BUFFER_BIT);
for (let i = 0; i < points.length; i++) {
var color = points[i].color;
//为片元着色器中的 u_Color 传递随机颜色
gl.uniform4f(u_Color, color.r, color.g, color.b, color.a);
//为顶点着色器中的 a_Position 传递顶点坐标。
gl.vertexAttrib2f(a_Position, points[i].x, points[i].y);
//绘制点
gl.drawArrays(gl.POINTS, 0, 1);
}
})
// 设置清屏颜色
gl.clearColor(0, 0, 0, 1.0);
// 用上一步设置的清空画布颜色清空画布。
gl.clear(gl.COLOR_BUFFER_BIT);
刚刚使用我采用 gl.vertexAttrib2f 直接给 a_Position 赋值,所以每绘制一个点,都要给着色器变量赋值一次,并且绘制一次,效率比较低。其实还有一种更快速的方式:利用缓冲区传递多个顶点数据。
好了,现在已经基本完成了预期的效果,很多方法其实有更简单更高级的方法,之后的学习中在持续优化…