体积光原理及WebGL实现
体积光(或叫上帝之光)在自然界中是十分常见的现象,如太阳光从云隙中透过时产生的云隙光,森林中阳光从树叶中穿过产生的光柱。
如果我们要在网页三维场景中模拟这种光效,需要深入了解大气物理模型和光散射原理。
大气物理模型
物体与其观察者之间存在着复杂的介质,比如太阳光到达我们眼睛是穿过了厚厚的大气层,大气层里面除了空气分子外,还包含云雨雾霾和灰尘等粒子。如果摄像机在地面上或者是在十分接近地面的位置,可以认为空气具有一个恒定的粒子密度,但准确而言,实际中的大气密度在地球引力的作用下,越靠近地表空气密度越高,越远离地表空气越稀薄。所以,我们假定空气粒子的密度是沿着海拔高度h呈指数递减的。按照经验,可以认为云雨雾霾尘埃等大颗粒粒子更多的存在于近地表的对流层中1200km以下,而在这之上到7994km之间为空气分子介质,在这个高度之外,我们认为近视于真空。
光散射
光在大气这种介质中传播会出现散射,散射按照粒子尺寸和光波长的相对关系如下图所示:
1. Rayleigh散射:
由空气中远小于波长的微粒(如空气分子)引起的散射称作瑞利散射。Rayleigh散射强度与光线波长的四次方成反比,这意味着白光中波长较短的颜色光(蓝色)会比波长较长的光(红色)有更强的散射强度,导致天空在白天偏向蓝色,而在黄昏偏向橙红色。 当日出或日落的时候,由于太阳的位置接近地平线,阳光斜射入大气,会在大气层中穿过很长的距离。在这个过程中,太阳光中的蓝色光几乎都会被散射殆尽无法抵达人眼,只剩下了波长较长的红色光,所以在太阳及其周围的天空都会呈现橘红色。
2. Mie散射:
在空气中直径与波长相当的微粒(如尘埃、雾滴等)所导致的散射现象称作Mie散射。与Rayleigh散射不同,Mie散射与波长无关,散射方向表现出明显的各向异性,光线会被粒子更多的向后方散射。而当阴雨天气时,空气中存在大量的水滴颗粒,Mie散射导致天空呈现灰白色。现今经常出现的雾霆天气,同样是因为空气中悬浮的大颗粒过多而导致的Mie散射现象。
结合上述的大气模型,我们可以认为Mie散射主要存在于1200km以下,而Rayleigh散射存在于7994km之下。
3. 内散射和外散射:
太阳光在大气中传输的时候会与空气中的微粒产生交互作用。有两种重要的交互方式:散射,它改变了光线的方向;吸收,它将光能吸收并转变为其它形态的能量(如热能)。而散射效果对场景中物体的影响又分为两个方面:一方面是一部分由物体反射的光被散射到视线之外,并不能到达摄像机,因而被衰减,称作外散射;另一方面是一部分太阳光被空气中的粒子散射正对向摄像机,这些正朝向视线的散射被称作内散射。
最后抵达视点被人眼所观察到的光线可分为两部分:衰减后的物体反射辐射度、被内散射的大气散射辐照度。
[方程1]
其中Lviewer为最终抵达摄像机的总光强,Lobject为物体的反射光(当视线不与物体相交时则为0),Linscatter为从O到C点路径上所有内散射光线的总和,这里暂时忽略太阳直射。
上面讲了大气物理模型和散射的物理原理,接下来看看我们具体该如何计算它。
为了计算每个像素的照明度,我们必须考虑光源到该像素的散射以及是否存在遮挡。
以阳光为例,我们从日光散射的分析模型开始,如下方程2:
[方程2]
上述方程式是方程1的具体化,s是光线穿过介质的距离,θ是视线和太阳光线之间的夹角。Esun是太阳源光,βex是由光吸收和外散射特性组成的消光常数,βsc是由瑞利和米氏散射特性组成的角散射项。该方程第一项计算从发射点到视点吸收到的光量,第二项计算由于光散射到视点射线路径而产生的附加量。由于阻塞物质(如云、建筑物和其他物体)而产生的影响在这里被简单地模拟为光照的衰减如方程3:
[方程3]
上述方程中,D(Φ)是太阳光线到视线位置之间不透明遮挡物的合成衰减率。
这样做引入了确定图像中每个点光源遮挡的复杂性。在屏幕上,我们没有完整的体积信息来决定遮挡。不过,我们可以通过在图像空间中,通过把从像素到光源的射线上的样本进行相加,估算每个像素的遮挡可能性。打到发射区域上的样本与打到遮挡物上的样本之间的比例,就是我们想要的遮挡百分比:D(Φ)。在发射区域比遮挡物亮度高的情况下,用该方法估算效果最好。如果我们把照明样本除以样本数目n,后处理(post-process)过程则可以化简为对图像取样进行求和,如方程式4:
[方程4]
更进一步,我们引入衰减系数来对求和结果进行参数化控制:
[方程5]
这里exposure控制后处理中的总体强度,weight控制每个样本的权重,decayi(介于[0, 1]之间)控制每个样本的衰减。这种指数式衰减因子实际上让每道光都能从光源处平滑的洒落下来。exposure和weight是简单的计量因子。增加它们其中任何一个都会在整体上增强计算出来的亮度。样本的weight通过微粒度(fine-grain)控制进行调整,exposure通过粗粒度(coarse-grain)控制进行调整。因为样本都是来自原图,不需要额外处理半透明物体。多光源的处理可以通过连续叠加屏间(screen-space)通道。虽然这个例子中,我们用的是日光分析模型,但实际上其它图像资源也适用。对于位于a点的太阳,以及每一个屏幕空间图像点φ,我们从原图开始,沿着射线矢量,按规定间隔,Δ(φ) = (φ-a)/n(density),连续地取样本然后求和。这里我们用密度(density)来控制样本间隔,这样可以在必要时减少样本迭代次数。当我们提高密度因子值时,样本间距相应减小,结果是光束更亮了,覆盖范围变短了。在下图中,来自φ1样本是没有被遮蔽的,结果就是正规评估L(s,θ)得到最大的散射照明。在φ2, 一部分样本沿途中碰到了建筑物,所以计算到的散射照明就少了。通过为图像中每一个像素进行射线求和,我们得出了包含遮挡物的光散射体结构。
有了以上这些公式,我们就可以编写相应的后处理着色器代码来进行光照计算了。
给定初始图像后,样本坐标沿着射线的方向,从像素点位置延伸到屏幕空间的光源位置上。屏幕空间中的光源位置是通过标准的world-view-project转换计算出来的,计量和偏移都在坐标[1-,1]的范围内。方程式5求和出来的连续样本L(s, θ, Φ ),通过weight常数和指数式衰减的衰减系数进行计量,目的是为了将控制效果的方法参数化。样本密度可以被调整以作为最终的控制因子,合成后的颜色值可以通过常量衰减系数exposure来缩放。
float4 main(float2 texCoord : TEXCOORD0) : COLOR0 { // Calculate vector from pixel to light source in screen space. half2 deltaTexCoord = (texCoord - ScreenLightPos.xy); // Divide by number of samples and scale by control factor. deltaTexCoord *= 1.0f / NUM_SAMPLES * Density; // Store initial sample. half3 color = tex2D(frameSampler, texCoord); // Set up illumination decay factor. half illuminationDecay = 1.0f; // Evaluate summation from Equation 3 NUM_SAMPLES iterations. for (int i = 0; i < NUM_SAMPLES; i++) { // Step sample location along ray. texCoord -= deltaTexCoord; // Retrieve sample at new location. half3 sample = tex2D(frameSampler, texCoord); // Apply sample attenuation scale/decay factors. sample *= illuminationDecay * Weight; // Accumulate combined color. color += sample; // Update exponential decay factor. illuminationDecay *= Decay; } // Output final color with a further scale control factor. return float4( color * Exposure, 1); }
事实上,屏幕空间采样并非只是遮挡采样,由于表面纹理的不同,会导致不理想的条纹出现。通常我们会采用遮挡预通道(pre-pass)和遮挡模板(stencil)来处理这些效果瑕疵。这里不做进一步的介绍。


- 相关文章
Monaco Editor 编辑器拷贝粘贴功能调用和获取选中文本
有时候需要在monaco editor外部调用编辑器的内置功能比如希望在页面主工具栏实现一些快捷操作。button
ES6小知识:动态对象键(Dynamic Object Keys)语法简介
在ES5,对象的键(key)总是被解释为字符串。ES6允许我们使用计算的值作为对象的键,使用方括号:[myKey]const
函数式JavaScript编程基础概念:Curry和Partial Application
本文介绍JS函数式编程中的两个概念:柯里(Curry)和部分应用程序(Partial Application)。什么是应用程序(Application)将函数应用于其参数以产生返回值的过...
前端开发框架技术选型:Angular2 VS React VS jQuery
Angular和React是主流的2个前端开发框架,但是严格来说两者并非对等的概念。Angular是一个基于MVC(或者MVVM)的框架,包含model(模型)/view(视图)/controll...
CSS3弹性布局弹性流(flex-flow)属性详解和实例
弹性布局是CSS3引入的强大的布局方式,用来替代以前Web开发人员使用的一些复杂而易错hacks方法(如使用float进行类似流式布局)。其中flex-flow是flex-direction...
深入理解Three.js(WebGL)贴图(纹理映射)和UV映射
本文将详细描述如何使用Three.js给3D对象添加贴图(Texture Map,也译作纹理映射,“贴图”的翻译要更直观,而“纹理映射”更准确。)。为了能够查看在线演示效...
Three.js 对象局部坐标转换为世界坐标
在Three.js中进行顶点几何计算时,一个需要注意的地方是,需要统一坐标系。比如你通过Three.js提供的API创建了一个球体网孔对象,那么默认情况下,各网孔顶点的...
三维向量的简单运算和实用意义
在WebGL的实际应用中我们广泛使用向量的几何运算来计算角度、距离,判断点线、点面之间的关系,比如物体之间的碰撞检测。本文简要介绍三维计算机图形学中常用的...
深度贴图(depth map)概念简介和生成流程
Depth map 深度图是一张2D图片,每个像素都记录了从视点(viewpoint)到遮挡物表面(遮挡物就是阴影生成物体)的距离,这些像素对应的顶点对于观察者而言是“可...
Three.js入门教程2 - 着色器(上)
WebGL入门教程5 - 详解纹理滤镜(Texture Filter)
WebGL中使用纹理贴图来实现细腻的物体表面观感,其中一个重要的参数是纹理滤镜(Texture Filter)。
这个参数用来处理当对象出现缩放时,纹理如何处理中间... 更多...