WebGL初级 : 在鼠标点击处绘制一个随机颜色的点。

参考资料

通过对《webGL入门与实践》一书为期一周的学习,仿照书中案例,终于写出了一个基本的webGL程序,也算掌握了webGL的基本绘制过程…

涉及到的相关术语解释:

本篇内容涉及一些术语,为了便于理解,以下是对涉及到的几个相关术语的简单解释:

  1. 图元:WebGL 能够绘制的基本图形元素,包含三种:线段三角形
  2. 片元:可以理解为像素,像素着色阶段是在片元着色器中。
  3. 裁剪坐标系:裁剪坐标系是顶点着色器中的 gl_Position 内置变量接收到的坐标所在的坐标系。
  4. 设备坐标系:又名 NDC 坐标系,是裁剪坐标系各个分量对 w 分量相除得到的坐标系,特点是 x、y、z 坐标分量的取值范围都在 [-1 , 1]之间,可以将它理解为边长为 2 的正方体,坐标系原点在正方体中心。

目标

实现一个简单的 WebGL 程序:鼠标点击一次,就会在点击位置处绘制一个随机颜色的点。麻雀虽小,但五脏俱全,使用 WebGL 绘制一个点虽然简单,但是它仍需要 JavaScript 程序和着色器程序共同完成。
所以通过这个例子,能够基本掌握 WebGL 的绘制过程。
效果如下:
WebGL初级 : 在鼠标点击处绘制一个随机颜色的点。_第1张图片

编写程序

1、准备着色器源码

着色器程序开始入手,先用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 呢?

2、准备 HTML 文件

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>
3、JavaScript 程序

首先,获取 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 程序都写完了,运行看下效果:
WebGL初级 : 在鼠标点击处绘制一个随机颜色的点。_第2张图片
上面这些代码实现了点的绘制。但是我们发现,在== 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,看来比之前简洁了很多。

4. 点的动态绘制

首先修改一下着色器程序,修改后的着色器程序要能够接收 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比较熟悉了,可以用一种更通用的转换方法:矩阵变换。

html部分
<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>
JavaScript 程序

这里省略了着色器创建部分…

//找到顶点着色器中的变量
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 赋值,所以每绘制一个点,都要给着色器变量赋值一次,并且绘制一次,效率比较低。其实还有一种更快速的方式:利用缓冲区传递多个顶点数据。
好了,现在已经基本完成了预期的效果,很多方法其实有更简单更高级的方法,之后的学习中在持续优化…

你可能感兴趣的:(学习了)