webgl性能优化初尝

栏目: 编程工具 · 发布时间: 6年前

内容简介:webgl性能优化初尝

上次文章介绍了如何用webgl快速创建一个自己的小世界,在我们入门webgl之后,并且可以用原生webgl写demo越来越复杂之后,大家可能会纠结一点:就是我使用webgl的姿势对不对。因为webgl可以操控shader加上超底层API,带来了一个现象就是同样一个东西,可以有多种的实现方式,而此时我们该如何选择呢?这篇文章将稍微深入一点webgl,给大家介绍一点webgl的优化知识。

讲webgl优化之前我们先简单回忆一下canvas2D的优化,常用的display list、动态区域重绘等等。用canvas2D多的同学应该对以上的优化或多或少都有了解,但是你对webgl的优化了解么,如果不了解的话往下看就对了~这里会先从底层图像是如何渲染到屏幕上开始,逐步开始我们的webgl优化。

gpu如何渲染出一个物体

先看一个简单的球的例子,下面是用webgl画出来的一个球,加上了一点光的效果,代码很简单,这里就不展开说了。

一个球

这个球是一个简单的3D模型,也没有复杂的一些变化,所以例子中的球性能很好,看FPS值稳定在60。后面我们会尝试让它变得复杂起来,然后进行一些优化,不过这一节我们得先了解渲染的原理,知其根本才能知道优化的原理。

我们都知道webgl与着色器是密不可分的关系,webgl当中有顶点着色器和片段着色器,下面用一张图来简单说明下一个物体由0到1生成的过程。

webgl性能优化初尝

0就是起点,对应图上面的3D mesh,在程序中这个就是3D顶点信息

1就是终点,对应图上面的Image Output,此时已经渲染到屏幕上了

我们重点是关注中间那三个阶段,第一个是一个标准的三角形,甚至三角形上面用三个圈指明了三个点,再加上vertex关键字,可以很明白的知道是顶点着色器处理的阶段,图翻译为大白话就是:

我们将顶点信息传给顶点着色器(drawElements/drawArray),然后着色器将顶点信息处理并开始画出三角形(gl_Position)

然后再看后两个图,很明显的fragments关键字指明了这是片元着色器阶段。Rasterization是光栅化,从图上直观的看就是三角形用三条线表示变成了用像素表示,其实实际上也是如此,更详细的可以看下面地址,这里不进行展开。

如何理解光栅化-知乎

后面阶段是上色,可以用textture或者color都可以,反正统一以rgba的形式赋给gl_FragColor

图中vertexShader会执行3次,而fragmentShader会执行35次(有35个方块)

发现fragmentShader执行次数远远超过vertexShader,此时机智的朋友们肯定就想到尽可能的将fragmentShader中的计算放在vertexShader中,但是能这样玩么?

强行去找还是能找到这样的场景的,比如说反射光。反射光的计算其实不是很复杂,但也稍微有一定的计算量,看核心代码

    vec3 L = normalize(uLightDirection);
    vec3 N = normalize(vNormal);
    float lambertTerm = dot(N, -L);
    vIs = vec4(0.0, 0.0, 0.0, 1.0);
    if(lambertTerm > 0.0) {
            vec3 E = normalize(vEye);
            vec3 R = reflect(L, N);
 
            float specular = pow(max(dot(R, E), 0.0), uShininess);
            vIs = uLightSpecular * uMaterialSpecular * specular;
    }
 

上面反射光代码就不细说了,核心就是内置的reflect方法。这段代码既可以放在fragmentShader中也可以放在vertexShader中,但是二者的结果有些不同,结果分别如下

放在vertexShader中

放在fragmentShader中

所以说这里的优化是有缺陷的,可以看到vertexShader中执行光计算和fragmentShader中执行生成的结果区别还是蛮大的。换言之如果想要实现图2的效果,必须在fragmentShader中去计算。开头就说了这篇文章的主题在 同样的一个效果,用什么方式是最优的 ,所以continue~

gpu计算能力很猛

上一节说了gpu渲染的原理,这里再随便说几个gpu相关的新闻

百度人工智能大规模采用gpuPhysX碰撞检测使用gpu提速 ……种种类似的现象都表明了gpu在单纯的计算能力上是超过普通的cpu,而我们关注一下前一节shader里面的代码

vertexShader

    void main() {
   vec4 vertex = uMMatrix * uRMatrix * vec4(aPosition, 1.0);
            vNormal = vec3(uNMMatrix * uNRMatrix * vec4(aNormal, 1.0));
            vEye = -vec3((uVMatrix * vertex).xyz);
 
            gl_Position = uPMatrix * uVMatrix * vertex;
    }
 

fragmentShader

    void main() {
    vec3 L = normalize(uLightDirection);
    vec3 N = normalize(vNormal);
    float lambertTerm = dot(N, -L);
 
    vec4 Ia = uLightAmbient * uMaterialAmbient;
    vec4 Id = vec4(0.0, 0.0, 0.0, 1.0);
    vec4 Is = vec4(0.0, 0.0, 0.0, 1.0);
 
    if(lambertTerm > 0.0) {
        Id = uLightDiffuse * uMaterialDiffuse * lambertTerm;
 
        vec3 E = normalize(vEye);
        vec3 R = reflect(L, N);
        float specular = pow(max(dot(R, E), 0.0), uShininess);
        Is = uLightSpecular * uMaterialSpecular * specular;
    }
 
    vec4 finalColor = Ia + Id + Is;
    finalColor.a = 1.0;
 
    gl_FragColor = finalColor;
}
 

可以发现逻辑语句很少,更多的都是计算,特别是矩阵的运算,两个mat4相乘通过js需要写成这样(代码来自glMatrix)

    mat4.multiply = function(mat, mat2, dest) {
    if(!dest) { dest = mat }
 
    // Cache the matrix values (makes for huge speed increases!)
    var a00 = mat[0], a01 = mat[1], a02 = mat[2], a03 = mat[3];
    var a10 = mat[4], a11 = mat[5], a12 = mat[6], a13 = mat[7];
    var a20 = mat[8], a21 = mat[9], a22 = mat[10], a23 = mat[11];
    var a30 = mat[12], a31 = mat[13], a32 = mat[14], a33 = mat[15];
 
    var b00 = mat2[0], b01 = mat2[1], b02 = mat2[2], b03 = mat2[3];
    var b10 = mat2[4], b11 = mat2[5], b12 = mat2[6], b13 = mat2[7];
    var b20 = mat2[8], b21 = mat2[9], b22 = mat2[10], b23 = mat2[11];
    var b30 = mat2[12], b31 = mat2[13], b32 = mat2[14], b33 = mat2[15];
 
    dest[0] = b00*a00 + b01*a10 + b02*a20 + b03*a30;
    dest[1] = b00*a01 + b01*a11 + b02*a21 + b03*a31;
    dest[2] = b00*a02 + b01*a12 + b02*a22 + b03*a32;
    dest[3] = b00*a03 + b01*a13 + b02*a23 + b03*a33;
    dest[4] = b10*a00 + b11*a10 + b12*a20 + b13*a30;
    dest[5] = b10*a01 + b11*a11 + b12*a21 + b13*a31;
    dest[6] = b10*a02 + b11*a12 + b12*a22 + b13*a32;
    dest[7] = b10*a03 + b11*a13 + b12*a23 + b13*a33;
    dest[8] = b20*a00 + b21*a10 + b22*a20 + b23*a30;
    dest[9] = b20*a01 + b21*a11 + b22*a21 + b23*a31;
    dest[10] = b20*a02 + b21*a12 + b22*a22 + b23*a32;
    dest[11] = b20*a03 + b21*a13 + b22*a23 + b23*a33;
    dest[12] = b30*a00 + b31*a10 + b32*a20 + b33*a30;
    dest[13] = b30*a01 + b31*a11 + b32*a21 + b33*a31;
    dest[14] = b30*a02 + b31*a12 + b32*a22 + b33*a32;
    dest[15] = b30*a03 + b31*a13 + b32*a23 + b33*a33;
 
    return dest;
};
 

可以说相比普通的加减乘除来说矩阵相关的计算量还是有点大的,而gpu对矩阵的计算有过专门的优化,是非常快的

所以我们第一反应肯定就是能在shader中干的活就不要让js折腾啦,比如说前面代码中将proMatrix/viewMatrix/modelMatrix都放在shader中去计算。甚至将modelMatrix里面再区分成moveMatrix和rotateMatrix可以更好的去维护不是么~

但是了解threejs或者看其他学习资料的的同学肯定知道threejs会把这些计算放在js中去执行,这是为啥呢??比如下方代码(节选自webgl编程指南)

vertexShader中

    ……
attribute vec4 u_MvpMatrix;
……
void main() {
    gl_Position = u_MvpMatrix * a_Position;
}
……
 

javascript中

    ……
var mvpMatrix = new Matrix4();
mvpMatrix.setPerspective(30, canvas.width/canvas.height, 1, 100);
mvpMatrix.lookAt(3, 3, 7, 0, 0, 0, 0, 1, 0);
gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);
……
 

这里居然把proMatrix/viewMatrix/modelMatrix全部在js中计算好,然后传入到shader中去,为什么要这样呢?

结合第一节我们看下vertexShader执行的次数是和顶点有关系的,而每个顶点都需要做对象坐标->世界坐标->眼睛坐标的变换,如果传入三个顶点,就代表gpu需要将proMatrix * viewMatrix * modelMatrix计算三次,而如果我们在js中就计算好,当作一个矩阵传给gpu,则是极好的。js中虽然计算起来相较gpu慢,但是胜在次数少啊。

看下面两个结果

在shader中计算

在js中计算

第一个是将矩阵都传入给gpu去计算的,我这边看到FPS维持在50左右

第二个是将部分矩阵计算在js中完成的,我这边看到FPS维持在60样的

这里用的180个球,如果球的数量更大,区别还可以更加明显。所以说gpu计算虽好,但不要滥用呦~

js与shader交互的成本

动画就是画一个静态场景然后擦掉接着画一个新的,重复不断。第一节中我们用的是setInterval去执行的,每一个tick中我们必须的操作就是更新shader中的attribute或者uniform,这些操作是很耗时的,因为是js和glsl程序去沟通,此时我们想一想,有没有什么可以优化的地方呢?

比如有一个场景,同样是一个球,这个球的材质颜色比较特殊

webgl性能优化初尝

x,y方向上都有着渐变,不再是第一节上面一个色的了,此时我们该怎么办?

首先分析一下这个这个球

webgl性能优化初尝

总而言之就是水平和垂直方向都有渐变,如果按之前的逻辑扩展,就意味着我们得有多个uniform去标识

我们先尝试一下,用如下的代码,切换uniform的方式

    ……
var colorArr = [];
var temp;
for(i = 1; i <= granularity; i++) {
    temp = 0.8 - (i / granularity * 0.7);
    for(j = 1; j <= granularity; j++) {
        colorArr.push([0.8 - (j / granularity * 0.7), temp, 0.1, 1.0]);
    }
}
……
for(i = 0; i < granularity; i++) {
    for(j = 0; j < granularity; j++) {
        webgl.uniform4fv(uMaterialDiffuse, colorArr[i * granularity + j]);
        webgl.drawElements(webgl.TRIANGLES, 6, webgl.UNSIGNED_SHORT, (i * granularity * 6 + j * 6) * 2);
    }
}
 

使用切换uniform的方式

发现FPS在40左右,还是蛮卡的。然后我们考虑一下,卡顿在哪?

vertexShader和fragmentShader执行的次数可以说都是一样的,但是uniform4fv和drawElements每一次tick中执行了多次,就代表着js与shader耗费了较大的时间。那我们应该如何优化呢?

核心在避免多次改变uniform,比方说我们可以尝试用attribute去代替uniform

看下结果怎样

使用attribute的方式

瞬间FPS就上去了对不~所以说灵活变通很重要,不能一味的死板,尽可能的减少js与shader的交互对性能的提高是大大有帮助的~

切换program的成本

上一节我们发现 频繁 切换切换uniform的开销比较大,有没有更大的呢?

当然有,那就是切换program,我们把之前的例子用切换program的方式试下,直接看下面的例子

点击前慎重,可能会引起浏览器崩溃

切换program

已经不需要关心FPS的了,可以直观的感觉到奇卡无比。切换program的成本应该是在webgl中开销是非常大的了,所以一定要少切换program

这里说的是少切换program,而不是说不要切换program,从理论上来说可以单个program写完整个程序的呀,那什么时候又需要切换program呢?

program的作用是代替if else语句,相当于把if else抽出来单独一个program,所以就是如果一个shader里面的if else多到开销超过program的开销,此时我们就能选择用program啦。

当然这里的度有点难把握,需要开发者自己多尝试,结合实际情况进行选择。这里有一个关于选择program还是if else的讨论,感兴趣的同学可以看看

(https://forums.khronos.org/showthread.php/7144-Performance-More-Shaderprograms-VS-IF-Statements-in-Shader)

结语

我们这里从原理触发,尝试了webgl的一些优化~如果你有什么建议和疑惑~欢迎留言讨论~


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

程序员2005精华本

程序员2005精华本

《程序员》杂志社 / 电子工业 / 2006-1 / 45.00元

本书为集结了《程序员》杂志与《msdn开发精选》杂志精华。分上、下两册,内容包括人物&报道、管理与实践、程序员手册、年鉴、《程序员》技术专题、《msdn开发精选》文章精选等。一起来看看 《程序员2005精华本》 这本书的介绍吧!

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具