GLSL着色器实现扫描效果

GLSL着色器其实是三维编程之中最实用的一个技术了,它其实就是OpenGL那一套语言,我现在实用GLSL不是做什么多么高端的底层开发或者是多么NB的图形效果,我仅仅想实现一个横向扫描的动态效果而已。这个效果其实在很多地方都在用但是我还没有在网上找到对应的案例那就只能自己实现了,先简单分享一下对应的效果(可能要加载一会儿毕竟我的服务器也不是特别的厉害)。

其实整体来看这个案例实际编辑的难度与思路都不算复杂只是一个思维方式的问题,以前我一直写的是三维部分的应用编程,对应就是在这一帧处理一件事下一帧更近上一帧的结果进行二次计算,本来一开始我也是按照这个思路进行对着色器的编辑,但是经过对着色器的较为整体的了解之后我发现人家的逻辑不是这样的,而是每一帧的状态相互之间独立而且着色器的内部也没有什么东西可以在下一帧复用,所以每一帧就必须要独立进行对所有的顶点与所有的片源进行约束才能实现最终的效果。

其实对于GLSL的概念是十分重要的,一般来讲编程的思路无论时面向过程还是面向对象都是一个以状态的改变为依托,流程便是对执行了什么实现了什么,对象就是修改了什么而已。但是GLSL之中你还不能按照这些思路来,因为GLSL是给底层CPU运行的一个语言这个东西其实写的是一个顶点、像素的状态,进入一个着色器之后便开始对所有的顶点执行你写在着色器之中的东西。所以如果不知道基础的一些概念你是什么都写不出来的。所以如果你从来就没有听说过什么是着色器的话我建议你先去了解一些不然后面的东西你理解起来会比较伤,当然如果你是天才就当我没有提。

线框显示

线框部分也是采用着色器实现的,以前我做一个项目需要实现对模型实体的显示同时也要对面向面的渲染,当时没有什么时间去学习GLSL所以干脆直接使用three.js里面的线框渲染同时将模型复制一份进行面的渲染现在想想很SB。

要实现线框的渲染就必须从原理开始,计算机图形在OpenGL之中就是一堆三角面,我们想让每一个三角面的边线独立渲染为一个颜色的话有一个东西非常的合适那就是三角形的重心坐标系,它的重心坐标可以这样定义:
三角形所在平面上的任意一点P(笛卡尔坐标),可以通过三角形的三个顶点A、B、C(笛卡尔坐标)来表示:
P = Ax + By + C * Z,其中(x、y、z)便是重心坐标。由此可以看出P点其实是A、B、C点加权之和。

如图所示,A点的重心坐标是(1,0,0),B点的重心坐标是(0,1,0),C点的重心坐标是(0,0,1)

重心坐标(x,y,z)中任何一个值为0的点,都在三角形的边上。不过在实际的图形渲染中,边的宽度不可能是0,而应该是一个大于0的值,所以一般可以指定一个要绘制的线宽width,如果任何一个点的重心坐标(x,y,z)中的人一个分量的值小于这个线宽width,可以认为在边上。我们将顶点的排序到三角形的三个顶点之中然后传入顶点着色器便可以实现了,这里可以直接将其写为几何体的属性供着色器使用:

var vectors = [
  new THREE.Vector3( 1, 0, 0 ),
  new THREE.Vector3( 0, 1, 0 ),
  new THREE.Vector3( 0, 0, 1 )
];
var position = geometry.attributes.position;
var centers = new Float32Array( position.count * 3 );
for ( var i = 0, l = position.count; i < l; i ++ ) {
  vectors[ i % 3 ].toArray( centers, i * 3 );
}
geometry.addAttribute( 'center', new THREE.BufferAttribute( centers, 3 ) );

顶点着色器

顶点部分只需要干三件事,一是讲UV坐标给片元着色器。第二是把对应的重心坐标给到片元着色器,第三也是最为基础的就是讲顶点使用屏幕空间矩阵投影到我们的屏幕之上。具体的代码如下:

//使用uniform传入时间值,这个值是单调递增的
uniform float time;
//使用几何体属性传入重心坐标
attribute vec3 center;
//将重心坐标与UV传到片元着色器
varying vec3 vCenter;
varying vec2 UV;

void main() {
    //赋值
    UV = uv;
    vCenter = center;
    //屏幕空间投影
    gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );

片元着色器

这里就稍微复杂一点了,毕竟我们着色器需要实现的效果几乎都是片元干的事,我就不解释什么了,通过代码与代码的注释已经足够说明问题了:

//获取时间值
uniform float time;
//获取顶点着色器传入的值
varying vec2 UV;
varying vec3 vCenter;
//定义函数获取像素在重心坐标中距离边线的距离
float edgeFactorTri() {
    //获取绝对距离
    vec3 d = fwidth( vCenter.xyz );
    //边线平滑
    vec3 a3 = smoothstep( vec3( 0.0 ), d * 2.0, vCenter.xyz );
    //获取xyz中的最小值
    return min( min( a3.x, a3.y ), a3.z );
}
void main() {
    //取0.1的值作为线框的宽度
    if(edgeFactorTri()<0.1){
        //定义初始的颜色值(单通道)
        float r = 0.5;
        //取时间小数点之后的数
        float x_now =fract(time);
        //负值置0,形成循环
        if(x_now<0.0){
            x_now = -x_now;
        }
        //如果像素点的位置大于时间值
        if(UV.x>x_now){
            //在扫描线前面的点设置为初始值
            r = 0.5;
        }
        //定义拖尾长度为整体长度的一半
        else if(UV.x<x_now-0.5){
            //超过一半设置为初始值
            r = 0.5;
        }
        else{
            //根据像素与扫描线的距离线性衰减
            r = 1.0 - (x_now -  UV.x) / 0.5 * 0.5 - 0.5;
            if(-0.05<x_now-UV.x&&x_now-UV.x<0.005){
                //将扫描线设置为0.005并高亮
                r+=0.5;
            }
            //设置当前像素颜色
            gl_FragColor = vec4(0.0, r, 0.0, 1);
            return;
        }
        //如果拖尾越过边缘
        if(x_now<0.5){
            if(UV.x>0.5)
            {
                //在另一端重现衰减
                r = (UV.x - x_now -0.5);
                if(-0.08<x_now-UV.x&&x_now-UV.x<0.008){
                    //将扫描线设置为0.005并高亮
                    r+=0.5;
                }
                //输入像素颜色
                gl_FragColor = vec4(0.0, r, 0.0, 1);
                return;
            }
        }
        //非扫描部分显示为纯黑
        gl_FragColor = vec4(0.0, 0.0, 0.0, 1);
    }
    else{
        //非扫描部分显示为纯黑
        gl_FragColor = vec4(0.0,0.0,0.0,1);
    }
}

大概就是这些东西完全实现了上图的效果,当然还需要注意UV,我是将模型的UV在max里面横向拉开实现的。效果其实还是很不错的,这只是一个很小的应用,GLSL是非常强大的一套图形编辑语言而且性能的高低全部看你代码的质量一般来讲较很多预封装的材质不知道好了多少倍。我这里也实力推荐只要你对计算机图形或者游戏之内的东西感兴趣又想靠这个兴趣吃饭那么GLSL是不得不学的一个东西。

留下回复