这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战
本文标题:WebGL第三十一课:利用多个格子对象模拟数码管
友情提示
这篇文章是WebGL课程专栏的第31篇,强烈建议从前面开始看起。因为花了大量的工夫来讲解向量的概念和矩阵运算。这些基础知识会影响你的思维。
本课代码直接跳转获取:三十一课代码
引子
上篇文章中,我们可以使用 shader 中的 uniform 变量,来针对不同的绘制对象进行不同的设置,从而得出单独的效果。例如颜色。
其实,拉伸,位移,旋转等,都可以使用 uniform 变量进行设置。而不用重新生成和传递buffer数据。这是好的,因为可以节省性能。
本篇文章,先实现拉伸,位移,旋转的uniform设置,然后使用多个格子对象,模拟一下数字电路中的数码管。
效果如下:
这里一共有7个格子,每一个格子都是可以独立控制的。我们把这七个格子排成数码管的形式。
从最下面的一个格子开始,逆时针往上走,最后一个格子就是最中间的那根。
七个格子的初始化
我们前两篇文章里,格子都有宽和高两个参数,在这里,我们把这两个概念换一下:
scalex 和 scaley
就是x方向的拉伸,和y方向的拉伸。
我们在给格子生成buffer数据的时候,也不依赖宽和高了,我们固定一下:
格子的左下角就是屏幕的左下角。 格子的右下角就是屏幕的右下角。 格子的左上角就是屏幕的左上角。 格子的右上角就是屏幕的右上角。
生成格子数据的代码如下:
this.data = [
// 第一个三角形
-1, -1, // 左下角点
1, -1, // 右下角点
1, 1, // 右上角点
// 第二个三角形
1, 1, // 右上角点
-1, 1, // 左上角点
-1, -1, // 左下角点
];
this.dataArr = new Float32Array(this.data);
为了让格子变成扁的或者长条的,我们可以利用 scalex 和 scaley。
先给出更新后的 constructor:
class GridObject {
// 宽 高 x坐标 y坐标
// 中心点是基准点
constructor(scalex, scaley, posx, posy) {
this.scalex = scalex;
this.scaley = scaley;
this.posx = posx;
this.posy = posy;
this.modelUpdated = false; // 模型是否更新,也就是说,是否需要重新绘制
this.glbuffer = null;
this.a_PointVertex = null;
this.color = { R: 0, G: 0, B: 0 };
this.rotate = 0;
}
...
}
好了,我们初始化的时候,直接 new 7个 GridObject 出来:
function generateDigitGrid(gl) {
//////////////////////////
let idx = 0;
for (; idx <= 6; idx++) {
let digit0 = new GridObject(0.3, 0.1, 0, 0, 0); // x方向压缩到0.3, y方向压缩到0.1
gridList.push(digit0);
}
gridList.forEach(element => {
element.genData(gl);
});
}
这样初始化的7个格子,都在一起,都是横的扁的。
可以利用rotate,让其中一些格子旋转90°,从而变成竖的。
利用html页面控件来修改格子的旋转和位置
我们上面利用一个全局变量 gridList 来存储7个格子的对象。
由于我们的目的是针对每一个格子都可以进行修改,所以我们需要加一个下拉列表空间,来选择目标:
<p>
<b>选取第几个格子</b>
<select id="chosen" oninput="chosen_selected()">
<option value="none">none</option>
<option value="0">0</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
</select>
</p>
如上代码所示,0-6, 七个格子,很合理。
然后把我们需要修改的参数,全部用控件弄出来:
<p>
<b>格子的x方向拉伸:</b>
<input id="gridscalex" type="range" min="0.1" max="1" value="1" step="0.01" oninput="gl_draw()" />
<b id="gridscalexvalue">0</b>
</p>
<p>
<b>格子的y方向拉伸:</b>
<input id="gridscaley" type="range" min="0.1" max="1" value="1" step="0.01" oninput="gl_draw()" />
<b id="gridscaleyvalue">0</b>
</p>
<p>
<b>格子的x方向位移:</b>
<input id="gridposx" type="range" min="-1" max="1" value="0" step="0.01" oninput="gl_draw()" />
<b id="gridposxvalue">0</b>
</p>
<p>
<b>格子的y方向位移:</b>
<input id="gridposy" type="range" min="-1" max="1" value="0" step="0.01" oninput="gl_draw()" />
<b id="gridposyvalue">0</b>
</p>
<p>
<b>格子的旋转(弧度):</b>
<input id="gridrotate" type="range" min="0" max="360" value="0" step="10" oninput="gl_draw()" />
<b id="gridrotatevalue">0</b>
</p>
我们的逻辑是,如果选取了某一个目标之后,以上的参数控件应该首先显示的是,选中GridObject的当前状态。
所以选中的回调函数 chosen_selected:
function chosen_selected() {
console.log(chosenDom.value);
if (chosenDom.value === "none") {
return;
}
//////////////////////////////////
gridscalexDom.value = gridList[chosenDom.value].scalex;
gridscaleyDom.value = gridList[chosenDom.value].scaley;
gridposxDom.value = gridList[chosenDom.value].posx;
gridposyDom.value = gridList[chosenDom.value].posy;
gridrotateDom.value = gridList[chosenDom.value].rotate;
gridscalexvalueDom.innerText = gridscalexDom.value;
gridscaleyvalueDom.innerText = gridscaleyDom.value;
gridposxvalueDom.innerText = gridposxDom.value;
gridposyvalueDom.innerText = gridposyDom.value;
gridrotatevalueDom.innerText = gridrotateDom.value;
}
如上所示,我们在选中一个目标格子之后,马上就可以通过各个参数控件,来查看当前的状态。 这里UI控件的代码真是很长很难写,没有双向绑定啥的,生写,也真是比较麻烦。
然后就是修改具体的参数了,我们观察一下其中旋转控件的回调函数:
<p>
<b>格子的旋转(角度):</b>
<input id="gridrotate" type="range" min="0" max="360" value="0" step="10" oninput="gl_draw()" />
<b id="gridrotatevalue">0</b>
</p>
我们看一下,gl_draw 函数的实现:
function gl_draw() {
gridscalexvalueDom.innerText = gridscalexDom.value;
gridscaleyvalueDom.innerText = gridscaleyDom.value;
gridposxvalueDom.innerText = gridposxDom.value;
gridposyvalueDom.innerText = gridposyDom.value;
gridrotatevalueDom.innerText = gridrotateDom.value;
gl.enable(gl.CULL_FACE);
gl.enable(gl.DEPTH_TEST);
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
if (chosenDom.value !== "none") {
gridList[chosenDom.value].scalex = gridscalexDom.value;
gridList[chosenDom.value].scaley = gridscaleyDom.value;
gridList[chosenDom.value].posx = gridposxDom.value;
gridList[chosenDom.value].posy = gridposyDom.value;
gridList[chosenDom.value].rotate = gridrotateDom.value;
}
gridList.forEach(element => {
element.render(gl, program);
});
}
我们可以看到,最前面就是就是控件本身的显示问题。
然后就是webgl的一系列的清空代码,就是把当前显示的全部清空,好重新绘制。
然后就是判断目标是否是none,如果不是none,就将全局变量gridList中的被选择项的参数,更新为当前控件中的值。
最后,就是一个普通的遍历,绘制。
这里一定要提一下,格子的绘制,也就是 render 函数:
render(gl, program) {
gl.bindBuffer(gl.ARRAY_BUFFER, this.glbuffer);
if (this.modelUpdated) {
this.modelUpdated = false;
if (this.a_PointVertex == null) {
this.a_PointVertex = gl.getAttribLocation(program, 'a_PointVertex');
}
}
//////////////////////////
gl.vertexAttribPointer(this.a_PointVertex, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(this.a_PointVertex);
gl.uniform3f(u_color, 1 - this.color.R, 1 - this.color.R, 1 - this.color.R);
//
let rad = ((2 * Math.PI) / 360) * this.rotate;
gl.uniformMatrix3fv(u_all_loc, false, genMat3ForGL(this.scalex,
this.scaley, rad, this.posx, this.posy
));
gl.drawArrays(gl.TRIANGLES, 0, this.pointCount);
}
我们可以看见其中颜色部分,我们只用了 R 分量,因为数码管不用太花里胡哨的。。(你可以搞的花里胡哨一点,我是不反对的。)
然后看一下角度如何转换成弧度,然后最重要的 如何将 拉伸,旋转,位移,柔和到一个矩阵里,然后将矩阵的值,传递给 uniform 变量 u_all_loc 。这里如果感兴趣的,可以翻看前面的文章,花了很多时间来讲这个矩阵的问题。
上述已经完成了一个目标,就是摆放不同的格子,使之长的像一个数码管......
接下来就是数码管的显示
数码管的显示,其实就不同的格子,亮灭。
比如说,0,这个数字,在数码管上显示的话,就是只有中间的一根不亮,其他的都是亮的。
再比如说,1,这个数字,就是最后边两个格子亮的,其他的都不亮。
依次类推,可以得到,9 个数字的分别对应的格子的亮灭情况,我们使用如下代码写出:
// 根据要显示的数字,决定数码管的每一位的亮度
function digitEncode(digit) {
if (digit == 0) {
gridList[0].color.R = 1;
gridList[1].color.R = 1;
gridList[2].color.R = 1;
gridList[3].color.R = 1;
gridList[4].color.R = 1;
gridList[5].color.R = 1;
gridList[6].color.R = 0;
return;
}
if (digit == 1) {
gridList[0].color.R = 0;
gridList[1].color.R = 1;
gridList[2].color.R = 1;
gridList[3].color.R = 0;
gridList[4].color.R = 0;
gridList[5].color.R = 0;
gridList[6].color.R = 0;
return;
}
// 后面的省略,读者可以自行用笔画一画,得出剩下的结果。
...
}
我们只需要在gl_draw函数的最后绘制之前,调用上面的 digitEncode函数,传入相应的数字,就可以让7个格子正确运转,显示相应的数字了:
function gl_draw() {
......
digitEncode(digitDom.value); // 下拉控件的值
gridList.forEach(element => {
element.render(gl, program);
});
}
上面省略了,下拉控件的代码,读者可自行补充。
到此,已经可以了,选取一个数字,就可以让数码管进行显示了!!! 看下面的图:
|
|
|
不得不说,有点意思!
整点花活,搞个定时器
我都懒得在代码里写了,直接在浏览器console里搞定:
得益于我的良好代码设计,全局变量用起来就是爽,所以我在console里两句话搞定:
let counter = 0;
setInterval(function(){ counter ++;digitDom.value = counter%10;gl_draw(); }, 1000);
上述代码直接放在浏览器console里,直接回车:
扩展一下这个代码,完全可以做一个挂在页面上的小时钟。
正文结束,下面是答疑
小瓜瓜说:我记得数字电路好像学过这个,现在忘了。
- 忘了就应该复习一下,是不是啊,搞搞吧。