近平面焦平面成像平面

GLSL-PathTracer

1
2
3
4
5
6
7
8
9
10
11
12
float scale = tan(camera.fov * 0.5);
d.y *= resolution.y / resolution.x * scale;
d.x *= scale;
vec3 rayDir = normalize(d.x * camera.right + d.y * camera.up + camera.forward);

vec3 focalPoint = camera.focalDist * rayDir;
float cam_r1 = rand() * TWO_PI;
float cam_r2 = rand() * camera.aperture;
vec3 randomAperturePos = (cos(cam_r1) * camera.right + sin(cam_r1) * camera.up) * sqrt(cam_r2);
vec3 finalRayDir = normalize(focalPoint - randomAperturePos);

Ray ray = Ray(camera.position + randomAperturePos, finalRayDir);

刚开始看这段代码的时候,没有想清楚代码里预设的内容,也即成像平面的距离是1;这里面注意两个地方,成像平面(的距离,z方向的位移)上的点来确定光线的方向,以及对焦距离确定对焦点。

Near Plane(近平面) 是裁剪用的,距离相机很近(比如 0.01),比它更近的物体不渲染。在光栅化管线中有意义,在 path tracing 中通常不需要。

Image Plane(成像平面) 是代码中构造 rayDir 的那个虚拟平面,距离相机为 1(因为 camera.forward 是单位向量,d.xd.y 是在这个平面上的偏移)。它的作用是把像素坐标转换为光线方向,跟最终画面的”对焦”无关。

focalDist(对焦距离) 是摄影概念,表示相机到”焦平面”的距离。焦平面上的物体是完全清晰的,偏离焦平面越远的物体越模糊(景深效果)。这个值一般比较大,比如 5.0、10.0,取决于你想对焦到场景中多远的位置,是针对于场景中的距离的概念。focalPoint计算这句,是在相机空间下计算的,其参与finalRayDir的计算为什么没有带上camera.position,因为一加一减,就不需要了。

smallpt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int main(int argc, char *argv[]){ 
int w=1024, h=768, samps = argc==2 ? atoi(argv[1])/4 : 1; // # samples
Ray cam(Vec(50,52,295.6), Vec(0,-0.042612,-1).norm()); // cam pos, dir
Vec cx=Vec(w*.5135/h), cy=(cx%cam.d).norm()*.5135, r, *c=new Vec[w*h];
#pragma omp parallel for schedule(dynamic, 1) private(r) // OpenMP
for (int y=0; y<h; y++){ // Loop over image rows
fprintf(stderr,"\rRendering (%d spp) %5.2f%%",samps*4,100.*y/(h-1));
for (unsigned short x=0, Xi[3]={0,0,y*y*y}; x<w; x++) // Loop cols
for (int sy=0, i=(h-y-1)*w+x; sy<2; sy++) // 2x2 subpixel rows
for (int sx=0; sx<2; sx++, r=Vec()){ // 2x2 subpixel cols
for (int s=0; s<samps; s++){
double r1=2*erand48(Xi), dx=r1<1 ? sqrt(r1)-1: 1-sqrt(2-r1);
double r2=2*erand48(Xi), dy=r2<1 ? sqrt(r2)-1: 1-sqrt(2-r2);
Vec d = cx*( ( (sx+.5 + dx)/2 + x)/w - .5) +
cy*( ( (sy+.5 + dy)/2 + y)/h - .5) + cam.d;
r = r + radiance(Ray(cam.o+d*140,d.norm()),0,Xi)*(1./samps);
} // Camera rays are pushed ^^^^^ forward to start in interior
c[i] = c[i] + Vec(clamp(r.x),clamp(r.y),clamp(r.z))*.25;
}
}
FILE *f = fopen("image.ppm", "w"); // Write image to PPM file.
fprintf(f, "P3\n%d %d\n%d\n", w, h, 255);
for (int i=0; i<w*h; i++)
fprintf(f,"%d %d %d ", toInt(c[i].x), toInt(c[i].y), toInt(c[i].z));
}

光线计算

1
Vec d = cx*(((sx+.5+dx)/2+x)/w-.5) + cy*(((sy+.5+dy)/2+y)/h-.5) + cam.d;

像素的 2×2 子像素结构: 每个像素被分成 2×2 的子像素(sx∈{0,1},sy∈{0,1})。对于子像素内的某一个采样点,它在像素内部的位置可以这样理解:

以 x 方向为例,(sx + 0.5 + dx) / 2 计算的是采样点在当前像素内的相对偏移(范围大致 [0, 1])。其中 sx + 0.5 是子像素的中心位置(sx=0 时为 0.5,sx=1 时为 1.5),dx 是 tent filter 产生的随机抖动(范围 [-1, 1]),除以 2 是因为一个像素被分成了 2 个子像素。所以当 sx=0 时,采样中心在像素内偏移 0.25 处;sx=1 时在 0.75 处,两个子像素的采样区域合理地覆盖了整个像素。

映射到归一化屏幕坐标: 加上像素坐标 x 后除以图像宽度 w,再减去 0.5,就得到了一个以图像中心为原点、范围约 [-0.5, 0.5] 的归一化坐标。y 方向同理。

合成方向向量: cxcy 是相机的”右”和”上”方向基向量,它们的长度编码了视场角(FOV)。cx = Vec(w * 0.5135 / h, 0, 0) 保证了宽高比正确,cy 的长度为 0.5135,对应垂直方向的半视场角约为 ((0.5135/2) °)。将归一化坐标分别乘以 cxcy,再加上相机的朝向 cam.d,就得到了这条采样光线的(未归一化的)方向向量 d

本质上这就是一个针孔相机模型cam.d 指向图像中心,cxcy 控制偏离中心的程度。

其中,cam.d = Vec(0,-0.042612,-1),x分离是0;Vec cx=Vec(w*.5135/h, 0, 0),cx的y、z分量也分别为0。cam.dcx是正交的。相机到成像平面的距离是1,cam.d是归一化后的向量,Vec d的计算中,x/y方向上的偏移后直接加的cam.d,其实是省略了cam.d * length。从光线的方向d的计算中,可得cy的跨度是0.5135,因此\(\tan\frac{fov}{2} = \frac{0.5135}{2}\)

光照计算

1
r = r + radiance(Ray(cam.o + d*140, d.norm()), 0, Xi) * (1./samps);

光线起点前推 (cam.o + d * 140): 相机实际位置在 ((50, 52, 295.6)),位于 Cornell Box 的外面(场景大致在 (z ) 范围内)。如果光线从相机原点出发,需要先穿过盒子的开口才能进入场景内部。通过沿方向 d 前推 140 个单位,光线起点被移到了大约 (z ) 的位置,也就是直接从盒子内部开始,避免了不必要的求交计算。代码注释 "Camera rays are pushed forward to start in interior" 说的就是这个意思。

采样平均 (* (1./samps)): 每个子像素内取 samps 个采样,每个采样的辐射贡献乘以 (1/) 后累加到 r 中,实现蒙特卡洛积分的均值估计。外层还有 c[i] = c[i] + Vec(clamp(r.x),clamp(r.y),clamp(r.z))*.25,即四个子像素各贡献 1/4。

参考