计算机3D渲染中的后处理post processing

后处理对于很多3D渲染的应用都是非常必要的,它有时候能够很大的增强图面的视觉体验,例如一些色彩的过滤,模糊,电影画面等;有时候还能实现常规渲染难以实现的功能,如绽放特效(bloom),边界检测与描绘等。

3D渲染的后处理本质,其实是2D图片的颜色处理与多张图片的定制化合成技术。其流程通常是这样的:首先通过常规的渲染将3d场景按照一种或多种指定的条件渲染出一张或者多张的2维图片;接着建立一个四边形,把这些图片的一张或者多张的数据作为纹理传递给这个四边形;这个四边形带着这些纹理数据将再次被放入gpu进行定制化的渲染;这个建立四边形,写入纹理渲染的步骤将会被重复多次;最后得到人们期望的图片。

通过这个描述,我们应该清楚,在后处理的过程中,我们主要是要利用gpu进行2维图像的合成和处理运算。也正是因为gpu比cpu更擅长做这个事,因此我们才会创建四边形,把数据传入gpu处理。否则这个图片处理的流程,放在cpu计算会更直接一些。

图形处理中的HDR与ToneMapping

在计算机图形渲染的后处理流程中,有时候会经常遇见所谓的ToneMap,所谓的色调映射。要解释色调映射要从HDR开始说起。因为ToneMap其实就是HDR的一个步骤。

HDR即High Dynamic Range。它通过多张照片合成一张大范围动态亮度的图片。无论是数码相机还是胶片相机都存在曝光的概念。其实HDR的D--dynamic指的即是曝光时间的不一样的动态效果。同样的环境,在同一个位置给景色拍照,在不同的曝光时间下得到的图片是不一样的。曝光时间越长,对该景色的阴影部分的细节记录越有利,对明亮的地方则越不利(明亮的地方就全白色了)。相反,曝光时间越长,对阴影部分越不利(阴影部分全黑),却对明亮部分越有利。HDR的第一步是结合这些不同曝光时间的下的图片数据合成一张明亮度范围更广的数据。假设每一张图片的某个像素点的某个RGB中的一个值只能从0取到255,那么,对三张图片合成之后,其值应该能取到更大的范围,例如从0取到1024。例如,一个点在低曝光时间下的值为200,那么在正常曝光下,该点取的值肯定要大于200。所以HDR这里的H和R,所谓的High Range就是这么来的。多张照片得到了更大范围的像素值。

那么问题来了,得到了这么大范围的值后,如何显示?要知道打印机,显示器通常能够显示某种颜色的范围只有0到255。HDR的合成的数值的范围有时候能高达0到100000. 这时候,就需要用到色调映射ToneMap了。把大范围的色调,映射到小范围的数值上。映射方法有很多种,最简单的线性映射,即是按照比例,对每个值做缩小(这也是一些图形引擎的默认方法)。但是这种方法对于追求美化的目的来做图形处理是不够的。映射的方法需要充分考虑人类眼睛对颜色的敏感度,人类对美感的感觉来调整。有时候我们更想看清阴暗的细节,有时候我们更想看清全面性的细节,这些都是说不准的。

最后,提一下threejs的ToneMap。threejs的WebGLRenderer是可以定义ToneMap的方法和各种参数,来模拟HDR的处理过程的。因为有时候人们也确实存在对3D场景图像的不同需求。如果再配合上一些后处理算法,例如boom,高通过滤等,我们就可控制ToneMap的一些参数。例如通过控制ToneMapExprosure就可以控制某些物体是否高亮,和一些特殊的效果。

行优先与列优先 Row- and Column-major

在2维矩阵的存储上,存在着这两个概念。尤其在图形学相关的工程项目中,经常会遇见行优先或列优先不明确而导致的错误。

所谓行优先,即是矩阵按照一行一行的方式存储。例如

[ A11 A12 A13 A14]
[A21 A22 A23 A24]
[A31 A32 A33 A34]
[A41 A42 A43 A44]

按照如下顺序存成数组:

matrix[A11, A12, A13, A14, A21, A22, A23, A24, A31, A32, A33, A34, A41, A42, A43, A44]

如果是列优先,则是如下顺序:

matrix[A11, A21, A31, A41, A12, A22, A32, A42, A13, A23, A33, A43, A14, A24, A34 , A44]

在已知的工程项目里,gltf文件,threejs都是列优先。freecad, opencascad是行优先

脱屏渲染抗锯齿

在很多渲染技术中,都能实现抗锯齿这样的功能。然而在很多脱屏渲染库中,少有直接提供抗锯齿的功能。

基本上有两条路可以走,他们是ssaa和msaa。关于他们的具体描述可以参考这里

这里仅仅简绍下,ssaa。super sampleing anti-aliasing。简单的说就是先放大,再缩小。初步渲染时先多渲染一些点,然后再缩小图片。这样可以比较好的解决问题。可以用如下python代码表述:

import PIL
import numpy as np
....
imgData # type(imgData) = numpy.array, shape = (w,h,3)
img = PIL.fromArray(imgData)
img = img.resize(sw,sh)
newImgData = np.array(img)