webgl径向模糊实现体积光
体积光介绍
首先,我们要确认一下什么是体积光。体积光通俗来说是我们能看见的”光路“,并不是所有灯光都会形成体积光效果,它是光照到大气中粒子散射后得到的效果(丁达尔效应)。我们有时候还会看到一束束光散开的效果,这是光在传播过程中遇到了障碍物(比如穿过云层、树木的光束)导致的。
根据物理原理,我们知道体积光是粒子散射的结果,如果我们用体素的思想来考虑体积光,我们所看到的某一点处的体积光颜色是眼睛到当前点的射线上,光路中所有粒子散射光的叠加。
体积光经常模拟Sun Shaft(太阳散射)的效果。
常用实现思路
常用的体积光实现思路包括:
- BillBoard贴片
BillBoard贴片很容易理解,用PHOTOSHOP生成一个随机的明暗条文,加上遮罩,让它看起来有光条的感觉。 - 径向模糊
径向模糊是一种后处理的方法,所谓后期处理就是在游戏画面渲染完毕之后,另外加一次渲染,类似于PHOTOSHOP,但处理的对象是每一帧游戏画面,因为速度要求多使用GPU计算。 - 光线追踪
近期伴随着渲染技术的进步,业界已经开始使用基于光线追踪、阴影贴图等更为精细的渲染技术来实现体积光的效果。
详细介绍可以参考下面这篇文章,上面介绍的内容来自这篇文章:
https://zhuanlan.zhihu.com/p/21425792
本文的重点是介绍通过径向模糊来实现体积光的效果。当然径向模糊的缺点十分明显,如果光源不在画面内,显然径向模糊是没办法执行的。因此径向模糊实现的体积光主要用来表现天空中日月星光散射的效果。
径向模糊实现体积光
径向模糊实现体积光的主要步骤大致如下:
- 正常渲染整个画面。
- 然后再一次渲染整个画面:使用指定颜色渲染发光的对象,使用黑色渲染其他对象(遮挡物)。
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进行径向模糊,有关径向模糊的内容,可以关注我上一篇文章。代码如下所示:
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;
径向模糊的内容和正常绘制内容进行叠加
要进行叠加,有人使用如下的思路:
- 把正常的场景绘制到一个framebuffer上面
- 把模糊后的效果绘制到另外一个framebuffer上面。
- 把上面两次绘制的贴图对象传递给一个叠加的绘制程序,绘制正常的结果。 一般来说,叠加程序会构造一个像素叠加方法,如下所示:
"void main() {",
"vec4 texel = texture2D( tDiffuse, vUv );",
"vec4 add = texture2D( tAdd, vUv );",
"gl_FragColor = texel + add * fCoeff;",
"}"
该方法的优点是,可以更加灵活的控制叠加算法,比如可以调整fCoeff参数调整体积光的强度;缺点也比较明显,多使用了两次framebuffer,性能消耗更大。
本示例不使用以上方法,而是使用如下思路:
- 正常绘制场景
- 开启webgl的addtive blend 功能
- 绘制模糊场景
代码如下所示:
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彪叔"观看,也欢迎关注公众号。

webgl径向模糊实现体积光的更多相关文章
- Unity Shader-GodRay,体积光(BillBoard,Volume Shadow,Raidal Blur,Ray-Marching)
好久没有更新博客了,经历了不少事情,好在最近回归了一点正轨,决定继续Unity Shader的学习之路.作为回归的第一篇,来玩一个比较酷炫的效果(当然废话也比较多),一般称之为GodRay(圣光),也 ...
- 【Unity Shaders】ShadowGun系列之二——雾和体积光
写在前面 体积光,这个名称是God Rays的中文翻译,感觉不是很形象.God Rays其实是Crepuscular rays在图形学中的说法,而Crepuscular rays的意思是云隙光.曙光. ...
- webgl实现径向模糊
径向模糊简介 径向模糊,是一种从中心向外呈幅射状,逐渐模糊的效果. 因此径向模糊经常会产生一些中心的发散效果,在PS中同样也有径向模糊的滤镜效果. 径向模糊通常也称为变焦模糊.径向模糊(Radial ...
- PBRT笔记(14)——光线传播2:体积渲染
传输公式 传输方程是控制光线在吸收.发射和散射辐射的介质中的行为的基本方程.它解释了第11章中描述的所有体积散射过程--吸收.发射和内.外散射.并给出了一个描述环境中辐射分布的方程.光传输方程实际上是 ...
- webGL动画
在做这个项目之前,我也和很多人的想法一样觉得:H5做动画性能不行,只能完成简单动画,可是事实并非如此.所以借此篇分享振奋下想在H5下做酷炫游戏的人心. 体验游戏请长按二维码识别: 好吧,知道你懒.不想 ...
- [转]显卡帝揭秘3D游戏画质特效
显卡帝揭秘3D游戏画质特效 近几年来,大量采用最新技术制作的大型3D游戏让大部分玩家都享受到了前所未有的游戏画质体验,同时在显卡硬件方面的技术革新也日新月异.对于经常玩游戏的玩家来说,可能对游戏画质提 ...
- Unity实现刺客信条灯光的思路探究
灯光需求 类似刺客信条的开场CG动画,场景中打着酷炫的灯光,玩家在场景中行走可以感受到灯光很真实. 参考视频:http://www.iqiyi.com/w_19rqytbmvt.html 运行环境 安 ...
- {转自MC}NVIDIA DirectX 11演示DEMO详解
http://tieba.baidu.com/p/1960826986 图形技术无论如何发展,最终都要落到实际的应用中才有效果.在个人电脑上,图形技术最大的用户除了显示UI和操作界面外,就是呈现美轮美 ...
- 移动平台Unity3D 应用性能优化
WeTest 导读 做了大概半年多VR应用了,VR由于双眼double渲染的原因,对性能的优化要求比较高,在项目的进展过程中,总结了一些关于移动平台上Unity3D的性能优化经验,供分享. 一.移动平 ...
- PBRT笔记(10)——体积散射
体散射处理过程 3个影响参与介质在环境中的辐射度分布的主要因素: 吸收:减少光能,并将其转化为别的能量,例如热量. 发光:由光子发射光能至环境中. 散射:由于粒子碰撞,使得一个方向的辐射度散射至其他方 ...
随机推荐
- salesforce零基础学习(一百三十九)Admin篇之Begins/Contains/Starts With 是否区分大小写
本篇参考: https://help.salesforce.com/s/articleView?id=sf.customize_functions_begins.htm&type=5 http ...
- 基于Ubuntu20.04在k8s 1.25部署gin+MySQL服务
0. 前言 某天突发奇想,既然都学了 docker 了,那干脆,顺便把 kubernetes 也学了,于是开始了我长达一个月的环境搭建.踩坑历程. 最开始,我的想法是,在我的物理机使用 WSL + d ...
- Js实现任意位置缩放图片,深入理解背后原理
前言 本文将用一个简单的例子详细讲解如何用原生JS一步步实现完整的任意位置缩放图片功能,无任何第三方依赖,指针事件 进行多端统一的事件监听,干货满满. 完整代码 为提升阅读体验,正文中代码展示有部分省 ...
- Qt-FFmpeg开发-视频播放(4)
音视频/FFmpeg #Qt Qt-FFmpeg开发-视频播放[软解码 + OpenGL显示YUV420P图像] 目录 音视频/FFmpeg #Qt Qt-FFmpeg开发-视频播放[软解码 + Op ...
- docker——容器的基本操作
docker 容器的基本操作 run 格式 docker run [选项] 镜像 [命令] [参数...] 选项 选项 解释 -d 后台运行 -i 交互模式 -t 分配一个伪终端 -p 设置端口 -- ...
- 使用Visual Studio分析.NET Dump
前言 内存泄漏和高CPU使用率是在日常开发中经常遇到的问题,它们可能会导致应用程序性能下降甚至崩溃.今天我们来讲讲如何使用Visual Studio 2022分析.NET Dump,快速找到程序内存泄 ...
- docker使用Open Policy Agent(OPA)进行访问控制
目录 一.系统环境 二.前言 三.Open Policy Agent 简介 四.Rego 语言简介 五.配置基本环境 六.docker安装OPA插件 6.1 安装docker 6.2 docker安装 ...
- Jmeter进行HTTPS接口压测及SSL证书验证
一.前言 使用JMeter压测HTTPS接口比较简单,只需要预先处理SSL证书认证,后面就是压测HTTP接口的通用步骤. HTTPS连接证书来验证浏览器和WEB服务器之间的连接.通过HTTP连接时,服 ...
- 有点儿神奇,原来vue3的setup语法糖中组件无需注册因为这个
前言 众所周知,在vue2的时候使用一个vue组件要么全局注册,要么局部注册.但是在setup语法糖中直接将组件import导入无需注册就可以使用,你知道这是为什么呢?注:本文中使用的vue版本为3. ...
- 详解Web应用安全系列(2)注入漏洞之XSS攻击
上一篇介绍了SQL注入漏洞,今天我们来介绍另一个注入漏洞,即XSS跨站脚本攻击.XSS 全称(Cross Site Scripting) 跨站脚本攻击, 是Web应用中常见的漏洞.指攻击者在网页中嵌入 ...