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彪叔公众号

所支持的Unity版本 5.2.0 及以上版本 WebGL Showcase | WebGL压力测试|文档|论坛 这个插件允许您通过生成真正容积的程序束来大大改善场景的照明。 这是模拟聚灯和手电筒的密度,深度和音量的完美,简单而便宜的方法。 简单高效的体积照明解决方案兼容各种平台(Windows PC,Mac OS X,Linux,WebGL,iOS,Android,VR)! 即使在移动设备上,也能为您的聚灯和手电筒模拟密度,深度和体积的完美,简单且便宜的方式! 它通过自动高效地生成真正的体积程序束来渲染高质量的线效果,从而极大地改善了场景的照明。 特征: - 真正的体积效果:即使你在束中也能工作。 - 非常容易使用和集成/需要零设置。 - 程序生成:一切都是在引擎盖下动态计算的。 - 在任何地方添加无限束:替代解决方案通常只需要实时灯:此插件不需要。您可以制作烘烤的量,甚至可以在没有任何线的情况下添加束。 - 动态3D噪声功能,用于模拟动画体积雾/雾/烟雾效果。 - 体积粉尘颗粒功能可模拟高度详细的防尘灯和微尘效果。 - 动态遮挡:可以通过移动几何体来阻挡束。 - 您可以实时移动和旋转束。 - 触发区域功能:您可以跟踪通过束的对象。 - 完全动态:在游戏时间内从脚本,动画师或时间轴更改或动画每个属性。 - Super FAST:不需要任何后处理,命令缓冲区和计算着色器:即使在移动设备和WebGL等低性能平台上也能很好地工作。 - VR Ready:支持Normal和VR Single pass立体声。 - 平滑交叉并与几何和相机混合。 - 自定义截头圆锥几何体。 - 支持许多图形变体:延迟和前向渲染路径,Gamma和线性颜色空间,HDR颜色,多种混合模式。 - 调整分层图层和图层顺序,以使用2D精灵调整束渲染。 - 开箱即用的透视和正交相机。 - 支持Unity内置雾。 - WYSIWYG:在场景视图中立即可以看到每个修改:无需在编辑器和播放模式之间切换以查看您的更改。 - 完整源代码可用/无DLL。束设置和处理通过功能强大的API完全暴露。 - 详细的文件。 - 支持从Unity 5.2到最新的2017.X和2018.X版本。 - 示例场景包括:展示演示。 请注意,此资产不是全屏后期处理/图像效果。这与Unity内置的Sun Shafts图像效果不相似。 相反,体积束将产生优化的几何形状和材料谱。这种技术有几个优点: - 更精细:独立精确定制每个束。 - 您可以在任何地方添加束,即使在没有线的地方也是如此。 - 当连接到聚灯时,它支持实时,烘焙和混合模式。 - 您可以渲染的束数量没有限制。 - 更容易与您自己的管道集成:无需与您自己的图像效果或后处理堆栈混合,没有命令缓冲区,不需要计算着色器功能。 - 运行得更快。没有后期处理添加到您的相机。 - 支持移动等低端平台。 如何使用它? 体积束设计非常易于使用。无需设置。您不必将多个对象链接在一起。您只需要使用一个简单的新组件。你可以通过2次点击添加一个新的束! 您可以通过调整一组用户友好的属性来精确定制每个束的渲染。为了获得更好看的效果,一些属性会自动绑定到附加的聚灯。 限制: 目前,此资产的当前版本有一些小的限制: - 此资产仅支持“聚灯”(形状像锥形的束)。不支持点源(线向各个方向平等)。 - “3D噪声”功能要求着色器功能等于或高于Shader Model 3.5 / OpenGL ES 3.0。 2012年之后发布的任何移动设备都应该支持它。 - 仅在Unity 5.5或更高版本上支持“体积粉尘颗粒”。 - “动态遮挡”功能计算遮挡的近似值,但尚不支持“部分遮挡”。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值