回顾

在上一节中我们成功地在canvas上绘制了一个尺寸为10px的点。虽然效果很简单,但为了完成它我们着实写了不少代码。此时你可能会想:”为什么设计它的人不能将它封装的高级些,比如draw(new Point(10))。这样开发效率肯定会快很多!“如果你已经有了一定的编程经验,你一定会想到为什么它会如此复杂。webgl绘图的高复杂度意味着它提供的是较为底层的工具和方法、说明它给予开发者的自由度相当高。如果你只是想要一些相对简单固定的场景,那大可使用tree.js、sim.js等类库来完成,但相应的你的自由度将大大降低。当下的趋势是虽然有一些公司选择了用第三方类库来帮助实现自己的产品,但在到了一定时期,需求已经扩大到超出了类库所能支持的极限时,它们依旧不得不选择使用原生的webgl来实现某些效果。久而久之,和写原生也大同小异了。这也说明学习原生webgl的重要性。

接下来让我们展开下一节的学习。上一节中我们将点的位置、尺寸及颜色信息都直接写死在了GLSL ES中。本节我们将在javaScript中使用一些手段传值给GLSL ES程序,完成点的位置、尺寸及颜色的渲染。为了使例子更生动形象、交互性更强,我们还加入了响应鼠标单击绘点的效果。

目标

将canvas分成四个象限。当鼠标在不同象限区域点击时,该点将会呈现出不同颜色。

创建H5模板

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Click Points</title>
<style>
body {
margin: 0;
}

canvas {
width: 100%;
height: 100%
}
</style>
</head>
<body onload="loaded()">
<div>
<canvas id="c" width="500" height="500"></canvas>
</div>
<script id="shader-vs" type="x-shader/x-vertex">
attribute vec4 a_Position;
void main(){
    gl_Position = a_Position;
    gl_PointSize = 5.0;
}

</script>
<script id="shader-fs" type="x-shader/x-fragment">
precision mediump float;
uniform vec4 u_FragColor;
void main(){
    gl_FragColor = u_FragColor;
}

</script>
<script src="../lib/detector.js"></script>
<script src="ClickPoints.js"></script>
</body>
</html>

模板和前一节的大同小异。注意着色器,它们发生了若干变化。

<script id="shader-vs" type="x-shader/x-vertex">
attribute vec4 a_Position;
void main(){
    gl_Position = a_Position;
    gl_PointSize = 5.0;
}

</script>
<script id="shader-fs" type="x-shader/x-fragment">
precision mediump float;
uniform vec4 u_FragColor;
void main(){
    gl_FragColor = u_FragColor;
}

</script>

使用attribute和uniform变量

我们的目标是,将位置信息从JavaScript程序中传给着色器。有两种方式可以做到这点:attribute变量uniform变量。使用哪个变量取决于数据本身,attribute传输的是那些与顶点相关的数据,而unifrom变量传输的是那些对于所有顶点都相同(或与顶点无关)的数据。

attribute变量是一种GLSL ES变量,被用来从外部向顶点着色器传输数据,只有顶点着色器能使用它。很遗憾片元着色器无法使用attribute变量。为了动态改变点的颜色,我们使用uniform变量将颜色信息传递给片元着色器。

注意:在申明u_FragColor变量之前我们还使用精度限定词(precision qualifier)来指定变量的范围(最大值与最小值)和精度。本例中为中等精度。后续章节中我们再来详细了解精度的问题。

./ClickPoints.js

var gl, canvas;

function loaded() {
canvas = document.getElementById("c");
if (!detect()) return;
initGL(canvas);
initShaders();
var a_Position = gl.getAttribLocation(gl.program, "a_Position");//得到a_Position变量的地址
a_Position == -1 && console.error("Failed to get a_Position");//地址不存在报错
var u_FragColor = gl.getUniformLocation(gl.program, "u_FragColor");//得到u_FragColor变量的地址
u_FragColor == null && console.error("Failed to get u_FragColor");//地址不存在报错

gl.clearColor(0.0, 0.0, 0.0, 1.0);//指定绘图区域的背景色
gl.clear(gl.COLOR_BUFFER_BIT);//将指定的缓冲器设定为预定的值

canvas.onmousedown = function (e) {
click(e, gl, canvas, a_Position, u_FragColor);//响应单击,绘点
}
}

var g_points = [];
var g_color = [];
function click(e, gl, canvas, a_Position, u_FragColor) {...}

function detect() {...}

function initGL(canvas) {...}

function initShaders() {...}

我们在js的loaded方法中做出了若干处修改,其意义如注释所描述。上节在清空背景后直接执行了gl.drawArrays来画点,本例中画点的操作需要在用户单击后进行。接下来让我们看下click函数都做了些什么。

click():响应单击回调,绘制点
function click(e, gl, canvas, a_Position, u_FragColor) {
var rect = e.target.getBoundingClientRect();
var cx = (e.clientX - rect.left) / canvas.clientWidth * 2 - 1,
cy = 1 - (e.clientY - rect.top) / canvas.clientHeight * 2;
g_points.push([cx, cy]);
g_color.push(getColor(cx, cy));
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);

for (var index = 0; index < g_points.length; index++) {
gl.vertexAttrib3f(a_Position, g_points[index][0], g_points[index][1], 0.0);
gl.uniform4f(u_FragColor, g_color[index][0], g_color[index][1], g_color[index][2], g_color[index][3]);
gl.drawArrays(gl.POINTS, 0, 1);
}

function getColor(x, y) {
var color = [1.0, 1.0, 1.0, 1.0];
if (x > 0 && y > 0) {
color = [1.0, 0.0, 0.0, 1.0];//第一象限,红色
} else if (x < 0 && y > 0) {
color = [0.0, 1.0, 0.0, 1.0];//第二象限,绿色
} else if (x < 0 && y < 0) {
color = [0.0, 0.0, 1.0, 1.0];//第三象限,蓝色
} else if (x > 0 && y < 0) {
color = [1.0, 0.0, 1.0, 1.0];//第四象限,黄色
}
return color;
}

}

在这里我们不得不面对一个很麻烦的问题。鼠标单击canvas得到的x,y坐标是基于canvas坐标系的,而为了将点画在webgl环境的绘图区域,必须使用webgl坐标系。canvas坐标系与webgl坐标系标准不同,必须将单击得到的x,y坐标转制成webgl坐标系下的x,y值才能正确显示点的位置。下图展示了两种坐标系。


坐标转制的相关代码:

var rect = e.target.getBoundingClientRect();
var cx = (e.clientX - rect.left) / canvas.clientWidth * 2 - 1,
cy = 1 - (e.clientY - rect.top) / canvas.clientHeight * 2;

对照着上图,式中变量分别为

//鼠标单击偏移量
e.clientX = outside_x + inside_x;
e.clientY = outside_y + inside_y;
//canvas偏移量
rect.left = outside_x;
rect.top = outside_y;
//canvas的宽高
canvas.clientWidth;
canvas.clientHeight;

cx和cy是专制后的webgl坐标。转换过程很简单,我相信你能理解!

转制完成后,我们将x、y坐标记录在数组g_points中,此外还将通过函数getColor得到的不同象限的不同颜色也记录在数组g_color中。

也许你想知道,为什么要把鼠标每次点击的位置都记录下来,而不是仅仅记录最近的一次。这是因为WebGL使用的是颜色缓冲区。

WebGL中的绘制操作实际上是在颜色缓冲区中进行绘制的,绘制结束后系统将缓冲区中的内容显示在屏幕上,然后颜色缓冲区就会被重置,其中内容会丢失。

for (var index = 0; index < g_points.length; index++) {
gl.vertexAttrib3f(a_Position, g_points[index][0], g_points[index][1], 0.0);
gl.uniform4f(u_FragColor, g_color[index][0], g_color[index][1], g_color[index][2], g_color[index][3]);
gl.drawArrays(gl.POINTS, 0, 1);
}

还记得click方法中的最后两个参数a_Position和u_FragColor吗?它们分别就是我们在GLSL ES中申明为attribute何uniform的两个变量的地址。
gl.vertexAttrib3f方法可以将其第2个开始的3个参数设置到第1个参数所在的地址中。注意,在顶点着色器中,我们将a_Position申明为vec4类型,但现在只传了3个分量值(x,y和z)。实际上如果传入的分量值与申明的有出入,这个方法会自动将空缺的分量值补上1.0。该处我们齐次坐标第四个分量一直都是1.0,因此可以省略。
vertecAttrib3f有一系列同组函数,该系列函数的任务是从JavaScript向顶点着色器中的Attribute变量传值。

vertexAttrib同族函数
gl.vertexAttrib1f(location,v0)
gl.vertexAttrib2f(location, v0, v1)
gl.vertexAttrib3f(location, v0, v1, v2)
gl.vertexAttrib4f(location, v0, v1, v2, v3)

类似的gl.uniform4f方法用于从JavaScript向着色器中的Uniform变量传值。

在for循环的每次迭代中,最后执行gl.drawArrays向颜色缓冲区绘制一个点。当所有迭代完成,颜色缓冲区中绘满了用户从第一次到最后一次单击的根据象限不同进行着色的点。

成果

https://tabrisk.github.io/webgl-getting-start/example/chapter_1/ClickPoints.html

results matching ""

    No results matching ""