有这样一个物理现象,镜头对准被摄物体对焦,在底片上有清晰的成像,然而,在焦点前后一段短距离内的物体,在底片上仍然能得到较清晰的成像,离焦点更远的地方,光线开始扩散,影象变成模糊的,形成一个扩大的圆。这个圆叫做弥散圆。在焦点前后短距离内仍能得到较清晰的成像,是因为在这段距离内,弥散圆的直径比较小,在底片上人眼识别不出来。人眼不能识别的最大弥散圆直径称为允许弥散圆直径。在焦点前后,允许弥散圆直径之间的距离叫做焦深,前焦深和后焦深对应的物距之差就是景深,即Depth of Field(DOF)。如下图:
图1 景深(Depth of field),弥散圆(Circle of confusion)
景深与镜头焦距、光圈及物距有密切的关系。如果摄影师为了突出主题,都会选择大光圈,中焦距摄影,这时,只有对焦主体有清晰成像。
知道景深的由来之后,我们就可以在实时渲染领域模拟景深效果。景深是一个PostProcess,一般是一下几步骤:
1.按普通方法渲染场景,但不是渲染到backbuffer,而是渲染到texture,供景深PostProcess使用。
2.把场景的深度渲染到texture,供景深PostProcess使用。当然了,如果API直接提供sample depth buffer的能力,这个步骤就可以省去。
3.根据场景每个像素的深度,调整像素的模糊程度,模拟景深效果。
以下是实现及细节。
步骤1及步骤2使用MRT方法同时进行,场景的texture使用X8R8G8B8格式,depth texture我建议使用浮点格式,例如R32F,这样做可以保证足够的精度,缺点是需要硬件支持;也可以使用RGB格式,不过要进行数据压缩,增加运算量。以下是示例程序的场景texture及depth texture(R32F):
图2 场景texture,茶煲使用Instancing在一次DrawIndexedPrimitive内完成所有渲染
图3 Depth texture 格式R32F
需要注意的是,这里的Depth Texture记录的是normalized device coordinate(NDC)下的depth。我在步骤3通过NDCDepth恢复view position,计算离view point的距离,决定像素的模糊程度。另一个需要注意的地方是,渲染场景texture时,可以使用硬件特性生成mipmap chain,用mipmap chain模拟弥散圆。步骤3根据view point距离,使用tex2Dlod拾取这个mipmap chain,实现像素模糊。这个方法是最简便的方法,效率最高,但也是效果最差的方法,本文后面将有效果对比。
步骤3,从depth texture恢复view position(具体方法见我的随笔《Deferred Shading》),计算view point的距离,再根据外部参数(对焦距离、模糊系数、景深深度)决定像素的模糊程度。如果在渲染场景texture时已生成mipmap chain,这里就可以使用tex2Dlod得到模糊的像素。如果没有生成mipmap chain,就要对场景texture进行模糊处理得到模糊texture。使用经过模糊处理的texture与原有的场景texture,根据之前计算出来的模糊程度进行线性插值,得到像素最终的颜色。注意,模糊处理所使用的filter直接影响景深效果,下面是mipmap chain和8次3x3 box filter的效果对比:图4 Mipmap chain 模糊,锯齿较严重
图5 3x3 box filter 模糊,效果很自然(示例程序没有对采样texture边缘做特殊处理)
这个是程序最终截图:
图6 最终截图
其中Focal distance是对焦距离,对焦距离不是焦距,对焦距离是指对焦物体到焦平面的距离。调整对焦距离,可以使近景或远景得到清晰成像。调整对焦距离其实就是相机里“对焦”这一过程,对焦并没有改变镜头的焦距。相机通过细微调整镜头与底片之间的距离,使被摄物体在底片上有清晰成像,以完成对焦动作。Blur factor是模糊系数,决定场景的最大模糊程度,这个由mipmap chain最顶层(如果使用mipmap chain实现模糊)或模糊处理过的场景texture决定。Depth of field调整景深大小。通过调整Blur factor及Depth of field可模拟镜头光圈效果,当然了,如果配合HDR就更逼真了。
不足之处:1.这种景深实现手段始终无法逼真模拟弥散圆效果(见下图)
弥散圆并不是简单的模糊,而且弥散圆的形状与光圈形状有关。2. 实际上,前后景深并不是对称的,通常前景深较浅,也就是说前景模糊程度比背景要厉害些。
最后,示例程序使用DXUT、DirectX9接口,硬件要求SM3.0、MRT(RT>= 2)、D3DFMT_R32F,纹理取自DirectX SDK。示例程序源代码在这里。