WebGL第三十一课:利用多个格子对象模拟数码管

666 阅读6分钟

这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战

本文标题:WebGL第三十一课:利用多个格子对象模拟数码管

友情提示

这篇文章是WebGL课程专栏的第31篇,强烈建议从前面开始看起。因为花了大量的工夫来讲解向量的概念和矩阵运算。这些基础知识会影响你的思维。

本课代码直接跳转获取:三十一课代码

引子

上篇文章中,我们可以使用 shader 中的 uniform 变量,来针对不同的绘制对象进行不同的设置,从而得出单独的效果。例如颜色。

其实,拉伸,位移,旋转等,都可以使用 uniform 变量进行设置。而不用重新生成和传递buffer数据。这是好的,因为可以节省性能。

本篇文章,先实现拉伸,位移,旋转的uniform设置,然后使用多个格子对象,模拟一下数字电路中的数码管。

效果如下:

image.png

这里一共有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);
    });
}

上面省略了,下拉控件的代码,读者可自行补充。

到此,已经可以了,选取一个数字,就可以让数码管进行显示了!!! 看下面的图:

image.png

image.png

不得不说,有点意思!

整点花活,搞个定时器

我都懒得在代码里写了,直接在浏览器console里搞定:

得益于我的良好代码设计,全局变量用起来就是爽,所以我在console里两句话搞定:

let counter = 0;
setInterval(function(){ counter ++;digitDom.value = counter%10;gl_draw(); }, 1000);

上述代码直接放在浏览器console里,直接回车:

counter.gif

扩展一下这个代码,完全可以做一个挂在页面上的小时钟。




  正文结束,下面是答疑

小瓜瓜说:我记得数字电路好像学过这个,现在忘了。

  • 忘了就应该复习一下,是不是啊,搞搞吧。