介绍pbrt中的camera代码大致实现。
Camera
class Camera {
public:
//实现相机在一定时间内进行特定的运动
AnimatedTransform CameraToWorld;
//快门开/关数据,可以用于计算动态模糊
const Float shutterOpen, shutterClose;
//胶片类指针用于计算最终图像
Film *film;
//介质类,表达相机所在的介质(在空气、水中)
const Medium *medium;
}
GenerateRay生成当前相机采样的一条光线,并且返回生成的光线对于最终图像的贡献值,一般都为1,除了有什么特殊计算
//除了与GenerateRay相同的功能外,还生成一个偏移一个UV像素的光线(RayDifferential)
//该操作与下一章采样有关
Float Camera::GenerateRayDifferential(const CameraSample &sample,
RayDifferential *rd) const {
Float wt = GenerateRay(sample, rd);
if (wt == 0) return 0;
//取得偏移光线
//这里的代码和书中不同,书中是取得偏移一个像素的光线
Float wtx;
//原来初始化列表可以作为临时对象
for (Float eps : { .05, -.05 }) {
CameraSample sshift = sample;
sshift.pFilm.x += eps;
Ray rx;
wtx = GenerateRay(sshift, &rx);
rd->rxOrigin = rd->o + (rx.o - rd->o) / eps;
rd->rxDirection = rd->d + (rx.d - rd->d) / eps;
if (wtx != 0)
break;
}
if (wtx == 0)
return 0;
Float wty;
for (Float eps : { .05, -.05 }) {
CameraSample sshift = sample;
sshift.pFilm.y += eps;
Ray ry;
wty = GenerateRay(sshift, &ry);
rd->ryOrigin = rd->o + (ry.o - rd->o) / eps;
rd->ryDirection = rd->d + (ry.d - rd->d) / eps;
if (wty != 0)
break;
}
if (wty == 0)
return 0;
rd->hasDifferentials = true;
return wt;
}
ProjectiveCamera
ProjectiveCamera继承自Camera类,实现了投影相机的相关功能。
这里需要需要注意一下,屏幕空间坐标与光栅化坐标的y轴方向是不同的,PBRT的屏幕空间还是一个三维坐标,所以y轴是向上的。而光栅化,与贴图的uv坐标方向相同,朝下。
在构造函数中初始化了
//相机坐标转屏幕坐标矩阵与逆矩阵
Transform CameraToScreen, RasterToCamera;
//屏幕空间坐标转光栅化坐标矩阵与逆矩阵
Transform ScreenToRaster, RasterToScreen;
//镜头半径、焦距
Float lensRadius, focalDistance;
//首先将坐标原点从左下移动左上,之后除以屏幕宽度获得标准化的坐标(0~1),再乘以贴图分辨率,获得光栅化的坐标
ScreenToRaster =
Scale(film->fullResolution.x, film->fullResolution.y, 1) *
Scale(1 / (screenWindow.pMax.x - screenWindow.pMin.x),
1 / (screenWindow.pMin.y - screenWindow.pMax.y), 1) *
Translate(Vector3f(-screenWindow.pMin.x, -screenWindow.pMax.y, 0));
OrthographicCamera
继承自ProjectiveCamera类。通过Orthographic函数生成正交矩阵,再用于初始化父类的CameraToScreen矩阵。
这个矩阵的会将摄像机的近平面(zNear)映射为0,远平面(zFar)映射为1。并将近平面移动至于z=0位置,随后对场景在z坐标进行缩放,也就是将其映射到0~1范围。可以理解为将近平面与远平面之间的场景放入一个长方体(z为0~1)的区域中。
Transform Orthographic(Float zNear, Float zFar) {
return Scale(1, 1, 1 / (zFar - zNear)) * Translate(Vector3f(0, 0, -zNear));
}
之后计算differential rays,因为正交相机的所生成的光线都是平行的,所以只需要移动原点即可。
dxCamera = RasterToCamera(Vector3f(1, 0, 0));
dyCamera = RasterToCamera(Vector3f(0, 1, 0));
因此我们可以很容易将光栅上的点转化为相机空间上的光线。(原点的x,y是确定的,z为zNear,方向是<0,0,1>)
如需景深效果,则根据快门开启时间(CameraSample::time)对光线进行调整,进而对景深进行模拟。最终返回的光线会被调整为世界空间的(毕竟求交操作都是世界空间的)
代码中为什么要在焦点平面进行采样偏移呢?PBRT后面的景深部分会解释,请见373页,看了图就明白了
```
Float OrthographicCamera::GenerateRay(const CameraSample &sample,
Ray ray) const {
ProfilePhase prof(Prof::GenerateCameraRay);
//计算光栅空间与摄像机空间的采样点
Point3f pFilm = Point3f(sample.pFilm.x, sample.pFilm.y, 0);
Point3f pCamera = RasterToCamera(pFilm); ray = Ray(pCamera, Vector3f(0, 0, 1));
//根据景深的相关参数改变光线
if (lensRadius > 0) {
//在一个区间为[-1,1]的圆形区域中进行均匀采样
Point2f pLens = lensRadius * ConcentricSampleDisk(sample.pLens);
//计算焦点平面上的点,首先计算焦距与方向分量z之间的比值(射线中的t),之后使用自定义的()操作符计算点位置
Float ft = focalDistance / ray->d.z;
Point3f pFocus = (*ray)(ft);
//更新光线
ray->o = Point3f(pLens.x, pLens.y, 0);
ray->d = Normalize(pFocus - ray->o);
}
//通过时间计算开门打开时间,并且存入ray中
ray->time = Lerp(sample.time, shutterOpen, shutterClose);
ray->medium = medium;
//转化为世界坐标
*ray = CameraToWorld(*ray);
return 1;
}
```
//对应版本的GenerateRayDifferential多了以下操作
if (lensRadius > 0) {
Point2f pLens = lensRadius * ConcentricSampleDisk(sample.pLens);
Float ft = focalDistance / ray->d.z;
//焦距平面上的偏移的点,并且分别计算x,y轴上的偏移数据
Point3f pFocus = pCamera + dxCamera + (ft * Vector3f(0, 0, 1));
ray->rxOrigin = Point3f(pLens.x, pLens.y, 0);
ray->rxDirection = Normalize(pFocus - ray->rxOrigin);
pFocus = pCamera + dyCamera + (ft * Vector3f(0, 0, 1));
ray->ryOrigin = Point3f(pLens.x, pLens.y, 0);
ray->ryDirection = Normalize(pFocus - ray->ryOrigin);
} else {
ray->rxOrigin = ray->o + dxCamera;
ray->ryOrigin = ray->o + dyCamera;
ray->rxDirection = ray->ryDirection = ray->d;
}
PerspectiveCamera
继承自ProjectiveCamera类。通过Perspective函数生成透视矩阵。
透视矩阵将相机空间上的点投影到图像平面上。投影点的x’、y’为相机空间中的x、y除以z坐标。投影后z’就被正规化为[0,1]。
//f为远剪裁面,n为近剪裁面
Transform Perspective(Float fov, Float n, Float f) {
// Perform projective divide for perspective projection
Matrix4x4 persp(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, f / (f - n), -f * n / (f - n),
0, 0, 1, 0);
//以弧度模式计算fov的tan值的倒数,再对之前计算的透视矩阵进行缩放
Float invTanAng = 1 / std::tan(Radians(fov) / 2);
return Scale(invTanAng, invTanAng, 1) * Transform(persp);
}
Float PerspectiveCamera::GenerateRay(const CameraSample &sample,
Ray *ray) const {
ProfilePhase prof(Prof::GenerateCameraRay);
Point3f pFilm = Point3f(sample.pFilm.x, sample.pFilm.y, 0);
//这里和OrthographicCamera略有不同其他的都一样
Point3f pCamera = RasterToCamera(pFilm);
*ray = Ray(Point3f(0, 0, 0), Normalize(Vector3f(pCamera)));
// Modify ray for depth of field
if (lensRadius > 0) {
Point2f pLens = lensRadius * ConcentricSampleDisk(sample.pLens);
Float ft = focalDistance / ray->d.z;
Point3f pFocus = (*ray)(ft);
ray->o = Point3f(pLens.x, pLens.y, 0);
ray->d = Normalize(pFocus - ray->o);
}
ray->time = Lerp(sample.time, shutterOpen, shutterClose);
ray->medium = medium;
*ray = CameraToWorld(*ray);
return 1;
}
对应的GenerateRayDifferential也和OrthographicCamera的差不多,仅仅是焦点平面上的采样点的计算有些改变。
景深
理想的针孔摄像机只允许光线通过一个点达到胶片。而现实世界中是不存在这种相机的。但是可以制作光圈非常小的相机来模拟,光圈较小的相机需要较大的曝光时间以准确成像,相关较大光圈的相机在快门打开时会导致照片中快速移动的物体变得模糊。同时光圈越大,景深越明显,同时曝光所需时间越短。
对于场景中的一点z,以及从焦距为f的薄镜头中观察该点所得的一点z’。存在以下关系公式:
弥散圆
引用自百度百科 在焦点前后,光线开始聚集和扩散,在一定范围外影象会变得模糊,形成一个扩大的圆,这个圆就叫做弥散圆。在现实当中,观赏拍摄的影象是以某种方式(比如投影、放大成照片等等)来观察的,人的肉眼所感受到的影象与放大倍率、投影距离及观看距离有很大的关系,如果弥散圆的直径小于人眼的鉴别能力,在一定范围内实际影象产生的模糊是不能辨认的。这个不能辨认的弥散圆就称为容许弥散圆(permissible circle of confusion)。
弥散圆受到光圈直径、焦距、物体与透镜之间的距离影响。
计算弥散圆的大小:观察书中图6.11可知几个变量直接的比例关系,得方程:
经过整理:
转换为场景深度后:
对简单镜头的模拟
光线追踪中对于简单镜头的建模如下:首先在镜头上选取一点,之后以该位置为起点,获取一条能使场景中处于对焦平面的物体,聚焦到胶片上的光线。(图6.13)
对于理想的针孔相机,对于胶片上的每一点(可以看成是一个像素)它的针孔透镜只准许一条光线通过。也就是说同一时间到达胶片上每一点的光线只有一条。
对于有限光圈的相机模型,我们先为每条光线在镜头上的圆盘区域中取得一个采样点,然后再计算穿过镜头中心并且使得对焦平面物体于胶片聚焦的光线。因为对焦平面的物体必然是聚焦的。所以光线始于采样点,相交于对焦平面。
环境相机
这里有用运用到球形坐标映射的问题,请见pbrt 5.5.2章。
GenerateRay将摄像机二维的采样点转化为三维的光线。
真实相机
这个挺复杂,直接跳过了