webgl径向模糊实现体积光

体积光介绍

首先,我们要确认一下什么是体积光。体积光通俗来说是我们能看见的”光路“,并不是所有灯光都会形成体积光效果,它是光照到大气中粒子散射后得到的效果(丁达尔效应)。我们有时候还会看到一束束光散开的效果,这是光在传播过程中遇到了障碍物(比如穿过云层、树木的光束)导致的。

根据物理原理,我们知道体积光是粒子散射的结果,如果我们用体素的思想来考虑体积光,我们所看到的某一点处的体积光颜色是眼睛到当前点的射线上,光路中所有粒子散射光的叠加。

体积光经常模拟Sun Shaft(太阳散射)的效果。

常用实现思路

常用的体积光实现思路包括:

  • BillBoard贴片
    BillBoard贴片很容易理解,用PHOTOSHOP生成一个随机的明暗条文,加上遮罩,让它看起来有光条的感觉。
  • 径向模糊
    径向模糊是一种后处理的方法,所谓后期处理就是在游戏画面渲染完毕之后,另外加一次渲染,类似于PHOTOSHOP,但处理的对象是每一帧游戏画面,因为速度要求多使用GPU计算。
  • 光线追踪
    近期伴随着渲染技术的进步,业界已经开始使用基于光线追踪、阴影贴图等更为精细的渲染技术来实现体积光的效果。

详细介绍可以参考下面这篇文章,上面介绍的内容来自这篇文章:
https://zhuanlan.zhihu.com/p/21425792

本文的重点是介绍通过径向模糊来实现体积光的效果。当然径向模糊的缺点十分明显,如果光源不在画面内,显然径向模糊是没办法执行的。因此径向模糊实现的体积光主要用来表现天空中日月星光散射的效果。

径向模糊实现体积光

径向模糊实现体积光的主要步骤大致如下:

  1. 正常渲染整个画面。
  2. 然后再一次渲染整个画面:使用指定颜色渲染发光的对象,使用黑色渲染其他对象(遮挡物)。
    3.对第二次渲染的画面进行径向模糊。
  3. 把模糊的画面和正常渲染的画面通过相机混合(Additively blend)得到最终的结果。

示例说明

本文示例中,渲染四个圆环物体和一个球形的发光物体。 四个圆环从上到下排列,发光物在在圆环中间周期性上下运动。

正常渲染整个画面

正常渲染整个画面不属于本文的重点内容,属于webgl的基本内容,此处不过多赘述。不过需要注意的一点是,发光物体使用纯色渲染,后面的效果才会好。

渲染发光物体和遮挡物

此处渲染的结果,我们称之为Occlusion buffer。为了获取Occlusion buffer,一般使用指定的颜色纯色绘制发光的球体,而使用黑色绘制其他的对象;我觉得更好的方式是,在渲染遮挡物的时候,通过colorMask指定不渲染颜色,只记录深度,因此起到遮挡的效果而不产生任何遮挡物的像素。代码如下所示:

 frameBuffer2.bind();
     gl.colorMask(false, false, false, false);
     for (var i = 0; i < 4; i++) {
       mat4.identity(mMatrix);
       ambientLightColor = hsva(i * 40, 1, 1, 1);
       mat4.translate(mMatrix, mMatrix, [0.0, 10.0 * i - 20, 0.0]);
       mat4.invert(invMatrix, mMatrix);
       mat4.mul(mvpMatrix, tmpMatrix, mMatrix);
       drawNormal(); // 绘制圆环
     }

     gl.colorMask(true, true, true, true);
     for (var i = 0; i < 1; i++) {
       mat4.identity(mMatrix);
       ambientLightColor = hsva(i * 40, 1, 1, 1);
       mat4.translate(mMatrix, mMatrix, [0.0, rad * 5 - 15, 0.0]);
       mat4.scale(mMatrix, mMatrix, [1.1, 1.1,1.1]);
       mat4.invert(invMatrix, mMatrix);
       mat4.mul(mvpMatrix, tmpMatrix, mMatrix);
       drawShpere(1);
     }
     frameBuffer2.unbind();

代码首先绑定一个framebuffer,因为Occlusion buffer是要绘制到贴图对象上的,有关framebuffer的内容此处不做详细说明,不明白的读者可以自行查找资料,也可以参考:渲染到纹理

之后开始循环绘制遮挡物,也就是圆环,此处循环了4次,表示绘制四个圆环。
需要注意的是,在绘制遮挡物之前,通过colorMask指定不绘制颜色到颜色缓冲区,也就是实际上不真正绘制圆环对象:

gl.colorMask(false, false, false, false);

既然不真正绘制圆环对象,为何要调用绘制代码呢,这是因为绘制的过程除了绘制颜色信息到颜色缓冲区,还会记录深度信息到深度缓冲区,而深度缓冲区可以记录最终的遮挡效果。 如果对于基本原理不懂的读者,可以自行查询相关知识,此处不赘述。也可以参考专栏内容:
https://xiaozhuanlan.com/webgl

然后开始绘制发光球体,需要注意的是,在绘制之前需要恢复颜色缓冲区的写入,所以先调用下面的代码进行恢复:

gl.colorMask(true, true, true, true);

然后绘制发光球体。

一个小技巧是,此处绘制发光球体的时候,适当的放大了球体的缩放比例:
mat4.scale(mMatrix, mMatrix, [1.1, 1.1,1.1]);
这是为了后期获取更明显的发光效果。

最终的绘制效果就是Occlusion buffer。如下图所示:


Occlusion buffer

可以看出值绘制了球体的部分,但是圆环对球体的遮挡仍然存在。

对Occlusion buffer进行径向模糊

上一节的内容,我们绘制了一个Occlusion buffer,此处对Occlusion buffer进行径向模糊,有关径向模糊的内容,可以关注我上一篇文章。代码如下所示:

function drawCopy(vv) {
    gl.useProgram(program2);
    gl.uniform1i(program2.texture, 1);
    gl.uniform2fv(program2.uCenterOffset, [vv[0],vv[1]]);
    gl.uniform1f(program2.strength, (document.getElementById('range').value | 0) / 2);

    gl.activeTexture(gl.TEXTURE0+1); //  激活gl.TEXTURE0
    gl.bindTexture(gl.TEXTURE_2D, frameBuffer2.colorTexture); // 绑定贴图对象

    gl.enableVertexAttribArray(program2.aPosition);
    gl.enableVertexAttribArray(program2.aTexCoord);

    gl.bindBuffer(gl.ARRAY_BUFFER, qdVerticesBuffer); //绑定缓冲区
    // 把缓冲区分配给attribute变量  
    gl.vertexAttribPointer(program2.aPosition, 3, gl.FLOAT, false, 0, 0);

    gl.bindBuffer(gl.ARRAY_BUFFER, qdStBuffer); //绑定缓冲区
    gl.vertexAttribPointer(program2.aTexCoord, 2, gl.FLOAT, false, 0, 0);


    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, qdIndexBuffer);
    //  gl.drawElements(gl.TRIANGLES,indices.length,gl.UNSIGNED_BYTE,0);
    gl.drawElements(gl.TRIANGLES, qdIndices.length, gl.UNSIGNED_SHORT, 0);
  }

需要注意的是:

  • 径向模糊的中心点不是固定的canvas的中心点,而应该是发光球体位置在屏幕上面的投影坐标位置:
    gl.uniform2fv(program2.uCenterOffset, [vv[0],vv[1]]);

vv的计算如下:

 let vv = vec4.create();
    vv[3] = 1;
      vec4.transformMat4(vv, vv, mvpMatrix);
      vv[0] = vv[0] / vv[3];
      vv[1] = vv[1] / vv[3];
      vv[2] = vv[2] / vv[3];
      vv[3] = 1;

径向模糊的内容和正常绘制内容进行叠加

要进行叠加,有人使用如下的思路:

  1. 把正常的场景绘制到一个framebuffer上面
  2. 把模糊后的效果绘制到另外一个framebuffer上面。
  3. 把上面两次绘制的贴图对象传递给一个叠加的绘制程序,绘制正常的结果。 一般来说,叠加程序会构造一个像素叠加方法,如下所示:
    "void main() {",

                "vec4 texel = texture2D( tDiffuse, vUv );",
                "vec4 add = texture2D( tAdd, vUv );",
                "gl_FragColor = texel + add * fCoeff;",

            "}"

该方法的优点是,可以更加灵活的控制叠加算法,比如可以调整fCoeff参数调整体积光的强度;缺点也比较明显,多使用了两次framebuffer,性能消耗更大。

本示例不使用以上方法,而是使用如下思路:

  1. 正常绘制场景
  2. 开启webgl的addtive blend 功能
  3. 绘制模糊场景

代码如下所示:

  gl.enable(gl.BLEND);
    gl.blendEquation(gl.FUNC_ADD);
    // gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
    gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
    ...
    drawCopy(vv);
    drawCopy(vv);

其中gl.blendFunc(gl.SRC_ALPHA, gl.ONE);指定了相加的混合方式。

注意上面drawCopy方法调用了两次,是为了加强体积光的效果。drawCopy调用次数和前面说到的fCoeff参数的作用类似。虽然增加了调用次数,但是由于drawCopy只是简单的绘制了贴图的内容,其性能损耗并不会太大。

有关性能优化

如果需要优化性能,可以考虑减少framebuffer的尺寸。
另外还可以通过降低模糊迭代次数来提高性能。

效果图

上面就是“webgl径向模糊实现体积光”的主要内容,下面上一张图看看渲染的效果:


体积光

本文也发表在我的webgl专栏,完整代码可以在专栏中获取:
https://xiaozhuanlan.com/topic/3148296057

案例视频 可以关注视频号 "ITman彪叔"观看,也欢迎关注公众号。


ITman彪叔公众号
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342